diff --git a/Barotrauma/BarotraumaClient/ClientCode.projitems b/Barotrauma/BarotraumaClient/ClientCode.projitems index 37b214100..8f73c6fd8 100644 --- a/Barotrauma/BarotraumaClient/ClientCode.projitems +++ b/Barotrauma/BarotraumaClient/ClientCode.projitems @@ -219,6 +219,7 @@ + diff --git a/Barotrauma/BarotraumaClient/ClientCode.shproj.user b/Barotrauma/BarotraumaClient/ClientCode.shproj.user index 0b0f24d53..966b4ffb6 100644 --- a/Barotrauma/BarotraumaClient/ClientCode.shproj.user +++ b/Barotrauma/BarotraumaClient/ClientCode.shproj.user @@ -1,5 +1,5 @@  - + true diff --git a/Barotrauma/BarotraumaClient/Properties/AssemblyInfo.cs b/Barotrauma/BarotraumaClient/Properties/AssemblyInfo.cs index c89a88b51..7e4c3bedf 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.9.4.0")] -[assembly: AssemblyFileVersion("0.9.4.0")] +[assembly: AssemblyVersion("0.9.5.1")] +[assembly: AssemblyFileVersion("0.9.5.1")] diff --git a/Barotrauma/BarotraumaClient/Source/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaClient/Source/Characters/AI/EnemyAIController.cs index 4d6c90904..d190e07af 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/AI/EnemyAIController.cs @@ -14,10 +14,27 @@ namespace Barotrauma Vector2 pos = Character.WorldPosition; pos.Y = -pos.Y; - if (SelectedAiTarget?.Entity != null) + if (State == AIState.Idle && PreviousState == AIState.Attack) { - GUI.DrawLine(spriteBatch, pos, new Vector2(SelectedAiTarget.WorldPosition.X, -SelectedAiTarget.WorldPosition.Y), Color.Red * 0.5f, 0, 4); - + var target = _selectedAiTarget ?? _lastAiTarget; + if (target != null) + { + var memory = GetTargetMemory(target); + Vector2 targetPos = memory.Location; + targetPos.Y = -targetPos.Y; + GUI.DrawLine(spriteBatch, pos, targetPos, Color.White * 0.5f, 0, 4); + GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 60.0f, $"{target.Entity.ToString()} ({memory.Priority.FormatZeroDecimal()})", Color.White, Color.Black); + } + } + else if (SelectedAiTarget?.Entity != null) + { + Vector2 targetPos = SelectedAiTarget.WorldPosition; + if (State == AIState.Attack) + { + targetPos = attackWorldPos; + } + targetPos.Y = -targetPos.Y; + GUI.DrawLine(spriteBatch, pos, targetPos, Color.Red * 0.5f, 0, 4); if (wallTarget != null) { Vector2 wallTargetPos = wallTarget.Position; @@ -26,7 +43,8 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, wallTargetPos - new Vector2(10.0f, 10.0f), new Vector2(20.0f, 20.0f), Color.Orange, false); GUI.DrawLine(spriteBatch, pos, wallTargetPos, Color.Orange * 0.5f, 0, 5); } - GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 60.0f, $"{SelectedAiTarget.Entity.ToString()} ({targetValue.FormatZeroDecimal()})", Color.Red, Color.Black); + GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 60.0f, $"{SelectedAiTarget.Entity.ToString()} ({GetTargetMemory(SelectedAiTarget).Priority.FormatZeroDecimal()})", Color.Red, Color.Black); + GUI.DrawString(spriteBatch, pos - Vector2.UnitY * 40.0f, $"({targetValue.FormatZeroDecimal()})", Color.Red, Color.Black); } /*GUI.Font.DrawString(spriteBatch, targetValue.ToString(), pos - Vector2.UnitY * 80.0f, Color.Red); @@ -42,6 +60,9 @@ namespace Barotrauma case AIState.Escape: stateColor = Color.LightBlue; break; + case AIState.Flee: + stateColor = Color.White; + break; case AIState.Eat: stateColor = Color.Brown; break; @@ -59,8 +80,8 @@ namespace Barotrauma 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, pos, + // ConvertUnits.ToDisplayUnits(new Vector2(LatchOntoAI.WallAttachPos.Value.X, -LatchOntoAI.WallAttachPos.Value.Y)), Color.Green, 0, 3); } } @@ -93,6 +114,17 @@ namespace Barotrauma } } } + else + { + if (steeringManager.AvoidDir.LengthSquared() > 0.0001f) + { + Vector2 hitPos = ConvertUnits.ToDisplayUnits(steeringManager.AvoidRayCastHitPosition); + hitPos.Y = -hitPos.Y; + + GUI.DrawLine(spriteBatch, hitPos, hitPos + new Vector2(steeringManager.AvoidDir.X, -steeringManager.AvoidDir.Y) * 100, Color.Red, width: 5); + //GUI.DrawLine(spriteBatch, pos, ConvertUnits.ToDisplayUnits(steeringManager.AvoidLookAheadPos.X, -steeringManager.AvoidLookAheadPos.Y), Color.Orange, width: 4); + } + } 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 613bf9d15..3e90775da 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/AI/HumanAIController.cs @@ -25,7 +25,7 @@ namespace Barotrauma { Vector2 pos = Character.WorldPosition; pos.Y = -pos.Y; - Vector2 textOffset = new Vector2(-40, -120); + Vector2 textOffset = new Vector2(-40, -160); if (SelectedAiTarget?.Entity != null) { @@ -33,26 +33,36 @@ namespace Barotrauma //GUI.DrawString(spriteBatch, pos + textOffset, $"AI TARGET: {SelectedAiTarget.Entity.ToString()}", Color.White, Color.Black); } + GUI.DrawString(spriteBatch, pos + textOffset, Character.Name, Color.White, Color.Black); + if (ObjectiveManager != null) { var currentOrder = ObjectiveManager.CurrentOrder; if (currentOrder != null) { - GUI.DrawString(spriteBatch, pos + textOffset, $"ORDER: {currentOrder.DebugTag} ({currentOrder.GetPriority().FormatZeroDecimal()})", Color.White, Color.Black); + GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 20), $"ORDER: {currentOrder.DebugTag} ({currentOrder.GetPriority().FormatZeroDecimal()})", Color.White, Color.Black); } else if (ObjectiveManager.WaitTimer > 0) { - GUI.DrawString(spriteBatch, pos + textOffset, $"Waiting... {ObjectiveManager.WaitTimer.FormatZeroDecimal()}", Color.White, Color.Black); + GUI.DrawString(spriteBatch, pos + new Vector2(0, 20), $"Waiting... {ObjectiveManager.WaitTimer.FormatZeroDecimal()}", Color.White, Color.Black); } var currentObjective = ObjectiveManager.CurrentObjective; if (currentObjective != null) { - GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 20), $"OBJECTIVE: {currentObjective.DebugTag} ({currentObjective.GetPriority().FormatZeroDecimal()})", Color.White, Color.Black); + if (currentOrder == null) + { + GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 20), $"MAIN OBJECTIVE: {currentObjective.DebugTag} ({currentObjective.GetPriority().FormatZeroDecimal()})", Color.White, Color.Black); + } var subObjective = currentObjective.SubObjectives.FirstOrDefault(); if (subObjective != null) { GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 40), $"SUBOBJECTIVE: {subObjective.DebugTag} ({subObjective.GetPriority().FormatZeroDecimal()})", Color.White, Color.Black); } + var activeObjective = ObjectiveManager.GetActiveObjective(); + if (activeObjective != null) + { + GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 60), $"ACTIVE OBJECTIVE: {activeObjective.DebugTag} ({activeObjective.GetPriority().FormatZeroDecimal()})", Color.White, Color.Black); + } } } @@ -81,7 +91,7 @@ namespace Barotrauma new Vector2(path.CurrentNode.DrawPosition.X, -path.CurrentNode.DrawPosition.Y), Color.BlueViolet, 0, 3); - GUI.DrawString(spriteBatch, pos + textOffset - new Vector2(0, 20), "Path cost: " + path.Cost.FormatZeroDecimal(), Color.White, Color.Black * 0.5f); + GUI.DrawString(spriteBatch, pos + textOffset + new Vector2(0, 80), "Path cost: " + path.Cost.FormatZeroDecimal(), Color.White, Color.Black * 0.5f); } } } diff --git a/Barotrauma/BarotraumaClient/Source/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/Source/Characters/Animation/Ragdoll.cs index c42fb71fb..ef4034a8b 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/Animation/Ragdoll.cs @@ -24,7 +24,7 @@ namespace Barotrauma partial void UpdateNetPlayerPositionProjSpecific(float deltaTime, float lowestSubPos) { - if (character != GameMain.Client.Character || !character.AllowInput) + if (character != GameMain.Client.Character || !character.CanMove) { //remove states without a timestamp (there may still be ID-based states //in the list when the controlled character switches to timestamp-based interpolation) @@ -92,10 +92,10 @@ namespace Barotrauma Collider.AngularVelocity = newAngularVelocity; float distSqrd = Vector2.DistanceSquared(newPosition, Collider.SimPosition); - float errorTolerance = character.AllowInput ? 0.01f : 0.2f; + float errorTolerance = character.CanMove ? 0.01f : 0.2f; if (distSqrd > errorTolerance) { - if (distSqrd > 10.0f || !character.AllowInput) + if (distSqrd > 10.0f || !character.CanMove) { Collider.TargetRotation = newRotation; SetPosition(newPosition, lerp: distSqrd < 5.0f, ignorePlatforms: false); @@ -108,9 +108,9 @@ namespace Barotrauma } } - //unconscious/dead characters can't correct their position using AnimController movement + //immobilized characters can't correct their position using AnimController movement // -> we need to correct it manually - if (!character.AllowInput) + if (!character.CanMove) { float mainLimbDistSqrd = Vector2.DistanceSquared(MainLimb.PullJointWorldAnchorA, Collider.SimPosition); float mainLimbErrorTolerance = 0.1f; diff --git a/Barotrauma/BarotraumaClient/Source/Characters/Character.cs b/Barotrauma/BarotraumaClient/Source/Characters/Character.cs index cf53dd16f..e855ea88c 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/Character.cs @@ -343,7 +343,7 @@ namespace Barotrauma partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult) { - if (attackResult.Damage <= 1.0f) { return; } + if (attackResult.Damage <= 1.0f || IsDead) { return; } if (soundTimer < soundInterval * 0.5f) { PlaySound(CharacterSound.SoundType.Damage); @@ -524,6 +524,8 @@ namespace Barotrauma partial void UpdateProjSpecific(float deltaTime, Camera cam) { + if (!enabled) { return; } + if (!IsDead && !IsUnconscious) { if (soundTimer > 0) diff --git a/Barotrauma/BarotraumaClient/Source/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/Source/Characters/CharacterInfo.cs index 0e7a8ad34..49fc72c13 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/CharacterInfo.cs @@ -50,7 +50,7 @@ namespace Barotrauma Job.Name, textColor: Job.Prefab.UIColor, font: font); } - if (personalityTrait != null && TextManager.Language == "English") + if (personalityTrait != null) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), TextManager.AddPunctuation(':', TextManager.Get("PersonalityTrait"), TextManager.Get("personalitytrait." + personalityTrait.Name.Replace(" ", ""))), font: font); @@ -125,7 +125,7 @@ namespace Barotrauma } } - partial void LoadAttachmentSprites() + partial void LoadAttachmentSprites(bool omitJob) { if (attachmentSprites == null) { @@ -139,7 +139,25 @@ namespace Barotrauma BeardElement?.Elements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.Beard))); MoustacheElement?.Elements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.Moustache))); HairElement?.Elements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.Hair))); - Job?.Prefab.ClothingElement?.Elements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.JobIndicator))); + if (omitJob) + { + JobPrefab.NoJobElement?.Element("PortraitClothing")?.Elements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.JobIndicator))); + } + else + { + Job?.Prefab.ClothingElement?.Elements("sprite").ForEach(s => attachmentSprites.Add(new WearableSprite(s, WearableType.JobIndicator))); + } + } + + // Doesn't work if the head's source rect does not start at 0,0. + public static Point CalculateOffset(Sprite sprite, Point offset) => sprite.SourceRect.Size * offset; + + public void CalculateHeadPosition(Sprite sprite) + { + if (sprite == null) { return; } + if (Head.SheetIndex == null) { return; } + Point location = CalculateOffset(sprite, Head.SheetIndex.Value.ToPoint()); + sprite.SourceRect = new Rectangle(location, sprite.SourceRect.Size); } public void DrawPortrait(SpriteBatch spriteBatch, Vector2 screenPos, float targetWidth) @@ -155,6 +173,10 @@ namespace Barotrauma // Scale down the head sprite 10% float scale = targetWidth * 0.9f / Portrait.size.X; Vector2 offset = Portrait.size * backgroundScale / 4; + if (Head.SheetIndex.HasValue) + { + Portrait.SourceRect = new Rectangle(CalculateOffset(Portrait, Head.SheetIndex.Value.ToPoint()), Portrait.SourceRect.Size); + } Portrait.Draw(spriteBatch, screenPos + offset, scale: scale, spriteEffect: SpriteEffects.FlipHorizontally); if (AttachmentSprites != null) { @@ -170,16 +192,21 @@ namespace Barotrauma public void DrawIcon(SpriteBatch spriteBatch, Vector2 screenPos, Vector2 targetAreaSize) { - if (HeadSprite != null) + var headSprite = HeadSprite; + if (headSprite != null) { - float scale = Math.Min(targetAreaSize.X / HeadSprite.size.X, targetAreaSize.Y / HeadSprite.size.Y); - HeadSprite.Draw(spriteBatch, screenPos, scale: scale); + float scale = Math.Min(targetAreaSize.X / headSprite.size.X, targetAreaSize.Y / headSprite.size.Y); + if (Head.SheetIndex.HasValue) + { + headSprite.SourceRect = new Rectangle(CalculateOffset(headSprite, Head.SheetIndex.Value.ToPoint()), headSprite.SourceRect.Size); + } + headSprite.Draw(spriteBatch, screenPos, scale: scale); if (AttachmentSprites != null) { float depthStep = 0.000001f; foreach (var attachment in AttachmentSprites) { - DrawAttachmentSprite(spriteBatch, attachment, HeadSprite, screenPos, scale, depthStep); + DrawAttachmentSprite(spriteBatch, attachment, headSprite, screenPos, scale, depthStep); depthStep += depthStep; } } @@ -188,13 +215,15 @@ namespace Barotrauma private void DrawAttachmentSprite(SpriteBatch spriteBatch, WearableSprite attachment, Sprite head, Vector2 drawPos, float scale, float depthStep, SpriteEffects spriteEffects = SpriteEffects.None) { - var list = AttachmentSprites.ToList(); if (attachment.InheritSourceRect) { if (attachment.SheetIndex.HasValue) { - Point location = (head.SourceRect.Location + head.SourceRect.Size) * attachment.SheetIndex.Value; - attachment.Sprite.SourceRect = new Rectangle(location, head.SourceRect.Size); + attachment.Sprite.SourceRect = new Rectangle(CalculateOffset(head, attachment.SheetIndex.Value), head.SourceRect.Size); + } + else if (Head.SheetIndex.HasValue) + { + attachment.Sprite.SourceRect = new Rectangle(CalculateOffset(head, Head.SheetIndex.Value.ToPoint()), head.SourceRect.Size); } else { @@ -219,7 +248,6 @@ namespace Barotrauma attachment.Sprite.Draw(spriteBatch, drawPos, Color.White, origin, rotate: 0, scale: scale, depth: depth, spriteEffect: spriteEffects); } - public static CharacterInfo ClientRead(string speciesName, IReadMessage inc) { ushort infoID = inc.ReadUInt16(); @@ -234,6 +262,8 @@ namespace Barotrauma string ragdollFile = inc.ReadString(); string jobIdentifier = inc.ReadString(); + int variant = inc.ReadByte(); + JobPrefab jobPrefab = null; Dictionary skillLevels = new Dictionary(); if (!string.IsNullOrEmpty(jobIdentifier)) @@ -249,7 +279,7 @@ namespace Barotrauma } // TODO: animations - CharacterInfo ch = new CharacterInfo(speciesName, newName, jobPrefab, ragdollFile) + CharacterInfo ch = new CharacterInfo(speciesName, newName, jobPrefab, ragdollFile, variant) { ID = infoID, }; diff --git a/Barotrauma/BarotraumaClient/Source/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/Source/Characters/CharacterNetworking.cs index f0e3a3aff..c8957a3b2 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/CharacterNetworking.cs @@ -249,7 +249,7 @@ namespace Barotrauma msg.ReadPadBits(); int index = 0; - if (GameMain.Client.Character == this && AllowInput) + if (GameMain.Client.Character == this && CanMove) { var posInfo = new CharacterStateInfo( pos, rotation, @@ -309,7 +309,7 @@ namespace Barotrauma LastNetworkUpdateID = controlled.LastNetworkUpdateID; } - Controlled = this; + if (!IsDead) { Controlled = this; } IsRemotePlayer = false; GameMain.Client.HasSpawned = true; GameMain.Client.Character = this; @@ -342,7 +342,7 @@ namespace Barotrauma } } - public static Character ReadSpawnData(IReadMessage inc, bool spawn = true) + public static Character ReadSpawnData(IReadMessage inc) { DebugConsole.Log("Reading character spawn data"); @@ -362,10 +362,9 @@ namespace Barotrauma Character character = null; if (noInfo) { - if (!spawn) return null; - character = Create(speciesName, position, seed, null, true); character.ID = id; + character.ReadStatus(inc); } else { @@ -375,15 +374,14 @@ namespace Barotrauma bool hasAi = inc.ReadBoolean(); string infoSpeciesName = inc.ReadString(); - if (!spawn) return null; - CharacterInfo info = CharacterInfo.ClientRead(infoSpeciesName, inc); - character = Create(infoSpeciesName, position, seed, info, GameMain.Client.ID != ownerId, hasAi); + character = Create(speciesName, position, seed, info, GameMain.Client.ID != ownerId, hasAi); character.ID = id; character.TeamID = (TeamType)teamID; + character.ReadStatus(inc); - if (character.IsHuman && character.TeamID != TeamType.FriendlyNPC) + if (character.IsHuman && character.TeamID != TeamType.FriendlyNPC && !character.IsDead) { CharacterInfo duplicateCharacterInfo = GameMain.GameSession.CrewManager.GetCharacterInfos().FirstOrDefault(c => c.ID == info.ID); GameMain.GameSession.CrewManager.RemoveCharacterInfo(duplicateCharacterInfo); @@ -394,7 +392,7 @@ namespace Barotrauma { GameMain.Client.HasSpawned = true; GameMain.Client.Character = character; - Controlled = character; + if (!character.IsDead) { Controlled = character; } GameMain.LightManager.LosEnabled = true; diff --git a/Barotrauma/BarotraumaClient/Source/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/Source/Characters/Health/CharacterHealth.cs index 2789f2703..4c98b4691 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/Health/CharacterHealth.cs @@ -13,7 +13,7 @@ namespace Barotrauma { private static bool toggledThisFrame; - private static Sprite damageOverlay; + public static Sprite DamageOverlay; private static string[] strengthTexts; @@ -153,12 +153,7 @@ namespace Barotrauma get { return healthBarPulsateTimer; } set { healthBarPulsateTimer = MathHelper.Clamp(value, 0.0f, 10.0f); } } - - static CharacterHealth() - { - damageOverlay = new Sprite("Content/UI/damageOverlay.png", Vector2.Zero); - } - + partial void InitProjSpecific(XElement element, Character character) { DisplayedVitality = MaxVitality; @@ -837,8 +832,8 @@ namespace Barotrauma 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)); + 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) @@ -997,30 +992,7 @@ namespace Barotrauma //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); - } + GetSuitableTreatments(treatmentSuitability, normalize: true, randomization: randomVariance); foreach (Affliction affliction in afflictions) { diff --git a/Barotrauma/BarotraumaClient/Source/Characters/Health/DamageModifier.cs b/Barotrauma/BarotraumaClient/Source/Characters/Health/DamageModifier.cs index 316872df3..c9258a8a0 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/Health/DamageModifier.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/Health/DamageModifier.cs @@ -2,7 +2,7 @@ { partial class DamageModifier { - [Serialize("", false)] + [Serialize("", false), Editable] public string DamageSound { get; diff --git a/Barotrauma/BarotraumaClient/Source/Characters/Limb.cs b/Barotrauma/BarotraumaClient/Source/Characters/Limb.cs index 1b0f04fa5..36cbc1b1e 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/Limb.cs @@ -119,7 +119,24 @@ namespace Barotrauma public List Deformations { get; private set; } = new List(); public Sprite Sprite { get; protected set; } - public DeformableSprite DeformSprite { get; protected set; } + + protected DeformableSprite _deformSprite; + + public DeformableSprite DeformSprite + { + get + { + var conditionalSprite = ConditionalSprites.FirstOrDefault(c => c.IsActive && c.DeformableSprite != null); + if (conditionalSprite != null) + { + return conditionalSprite.DeformableSprite; + } + else + { + return _deformSprite; + } + } + } public List DecorativeSprites { get; private set; } = new List(); @@ -127,15 +144,14 @@ namespace Barotrauma { get { - // TODO: should we optimize this? No need to check all the conditionals each time the property is accessed. - var conditionalSprite = ConditionalSprites.FirstOrDefault(c => c.IsActive); + var conditionalSprite = ConditionalSprites.FirstOrDefault(c => c.IsActive && c.ActiveSprite != null); if (conditionalSprite != null) { - return conditionalSprite; + return conditionalSprite.ActiveSprite; } else { - return DeformSprite != null ? DeformSprite.Sprite : Sprite; + return _deformSprite != null ? _deformSprite.Sprite : Sprite; } } } @@ -215,40 +231,53 @@ namespace Barotrauma DamagedSprite = new Sprite(subElement, file: GetSpritePath(subElement, Params.damagedSpriteParams)); break; case "conditionalsprite": - ConditionalSprites.Add(new ConditionalSprite(subElement, character, file: GetSpritePath(subElement, null))); + var conditionalSprite = new ConditionalSprite(subElement, character, file: GetSpritePath(subElement, null)); + ConditionalSprites.Add(conditionalSprite); + if (conditionalSprite.DeformableSprite != null) + { + CreateDeformations(subElement.GetChildElement("deformablesprite")); + } break; case "deformablesprite": - DeformSprite = new DeformableSprite(subElement, filePath: GetSpritePath(subElement, Params.deformSpriteParams)); - foreach (XElement animationElement in subElement.Elements()) - { - int sync = animationElement.GetAttributeInt("sync", -1); - SpriteDeformation deformation = null; - if (sync > -1) - { - // if the element is marked with the sync attribute, use a deformation of the same type with the same sync value, if there is one already. - string typeName = animationElement.GetAttributeString("type", "").ToLowerInvariant(); - deformation = ragdoll.Limbs - .Where(l => l != null) - .SelectMany(l => l.Deformations) - .Where(d => d.TypeName == typeName && d.Sync == sync) - .FirstOrDefault(); - } - if (deformation == null) - { - deformation = SpriteDeformation.Load(animationElement, character.SpeciesName); - if (deformation != null) - { - ragdoll.SpriteDeformations.Add(deformation); - } - } - if (deformation != null) Deformations.Add(deformation); - } + _deformSprite = new DeformableSprite(subElement, filePath: GetSpritePath(subElement, Params.deformSpriteParams)); + CreateDeformations(subElement); break; case "lightsource": LightSource = new LightSource(subElement); InitialLightSourceColor = LightSource.Color; break; } + + void CreateDeformations(XElement e) + { + foreach (XElement animationElement in e.GetChildElements("spritedeformation")) + { + int sync = animationElement.GetAttributeInt("sync", -1); + SpriteDeformation deformation = null; + if (sync > -1) + { + // if the element is marked with the sync attribute, use a deformation of the same type with the same sync value, if there is one already. + string typeName = animationElement.GetAttributeString("type", "").ToLowerInvariant(); + deformation = ragdoll.Limbs + .Where(l => l != null) + .SelectMany(l => l.Deformations) + .Where(d => d.TypeName == typeName && d.Sync == sync) + .FirstOrDefault(); + } + if (deformation == null) + { + deformation = SpriteDeformation.Load(animationElement, character.SpeciesName); + if (deformation != null) + { + ragdoll.SpriteDeformations.Add(deformation); + } + } + if (deformation != null) + { + Deformations.Add(deformation); + } + } + } } } @@ -260,11 +289,11 @@ namespace Barotrauma var source = Sprite.SourceElement; Sprite = new Sprite(source, file: GetSpritePath(source, Params.normalSpriteParams)); } - if (DeformSprite != null) + if (_deformSprite != null) { - DeformSprite.Remove(); - var source = DeformSprite.Sprite.SourceElement; - DeformSprite = new DeformableSprite(source, filePath: GetSpritePath(source, Params.deformSpriteParams)); + _deformSprite.Remove(); + var source = _deformSprite.Sprite.SourceElement; + _deformSprite = new DeformableSprite(source, filePath: GetSpritePath(source, Params.deformSpriteParams)); } if (DamagedSprite != null) { @@ -275,9 +304,8 @@ namespace Barotrauma for (int i = 0; i < ConditionalSprites.Count; i++) { var conditionalSprite = ConditionalSprites[i]; + var source = conditionalSprite.ActiveSprite.SourceElement; conditionalSprite.Remove(); - var source = conditionalSprite.SourceElement; - // TODO: lazy load? ConditionalSprites[i] = new ConditionalSprite(source, character, file: GetSpritePath(source, null)); } for (int i = 0; i < DecorativeSprites.Count; i++) @@ -289,6 +317,12 @@ namespace Barotrauma } } + private void CalculateHeadPosition(Sprite sprite) + { + if (type != LimbType.Head) { return; } + character.Info?.CalculateHeadPosition(sprite); + } + private string GetSpritePath(XElement element, SpriteParams spriteParams) { string texturePath = element.GetAttributeString("texture", null); @@ -330,7 +364,7 @@ namespace Barotrauma bool isFlipped = dir == Direction.Left; Sprite?.LoadParams(Params.normalSpriteParams, isFlipped); DamagedSprite?.LoadParams(Params.damagedSpriteParams, isFlipped); - DeformSprite?.Sprite.LoadParams(Params.deformSpriteParams, isFlipped); + _deformSprite?.Sprite.LoadParams(Params.deformSpriteParams, isFlipped); for (int i = 0; i < DecorativeSprites.Count; i++) { DecorativeSprites[i].Sprite?.LoadParams(Params.decorativeSpriteParams[i], isFlipped); @@ -461,24 +495,31 @@ namespace Barotrauma enableHuskSprite && HuskSprite != null && HuskSprite.HideLimb || OtherWearables.Any(w => w.HideLimb) || wearingItems.Any(w => w != null && w.HideLimb); + + var activeSprite = ActiveSprite; + if (type == LimbType.Head) + { + CalculateHeadPosition(activeSprite); + } + // TODO: there's now two calls to this, because body.Draw() method calls this too -> is this an issue? body.UpdateDrawPosition(); if (!hideLimb) { - var activeSprite = ActiveSprite; - if (DeformSprite != null && activeSprite == DeformSprite.Sprite) + var deformSprite = DeformSprite; + if (deformSprite != null) { if (Deformations != null && Deformations.Any()) { - var deformation = SpriteDeformation.GetDeformation(Deformations, DeformSprite.Size); - DeformSprite.Deform(deformation); + var deformation = SpriteDeformation.GetDeformation(Deformations, deformSprite.Size); + deformSprite.Deform(deformation); } else { - DeformSprite.Reset(); + deformSprite.Reset(); } - body.Draw(DeformSprite, cam, Vector2.One * Scale * TextureScale, color, Params.MirrorHorizontally); + body.Draw(deformSprite, cam, Vector2.One * Scale * TextureScale, color, Params.MirrorHorizontally); } else { @@ -489,15 +530,16 @@ namespace Barotrauma if (LightSource != null) { LightSource.Position = body.DrawPosition; + if (LightSource.ParentSub != null) { LightSource.Position -= LightSource.ParentSub.DrawPosition; } LightSource.LightSpriteEffect = (dir == Direction.Right) ? SpriteEffects.None : SpriteEffects.FlipVertically; } if (damageOverlayStrength > 0.0f && DamagedSprite != null && !hideLimb) { DamagedSprite.Draw(spriteBatch, new Vector2(body.DrawPosition.X, -body.DrawPosition.Y), - color * Math.Min(damageOverlayStrength, 1.0f), ActiveSprite.Origin, + color * Math.Min(damageOverlayStrength, 1.0f), activeSprite.Origin, -body.DrawRotation, - Scale, spriteEffect, ActiveSprite.Depth - 0.0000015f); + Scale, spriteEffect, activeSprite.Depth - 0.0000015f); } foreach (var decorativeSprite in DecorativeSprites) { @@ -636,48 +678,58 @@ namespace Barotrauma { foreach (var modifier in damageModifiers) { - float rotation = -body.TransformedRotation + GetArmorSectorRotationOffset(modifier.ArmorSectorInRadians) * Dir; - Vector2 forward = VectorExtensions.Forward(rotation); + //Vector2 up = VectorExtensions.Backward(-body.TransformedRotation + Params.GetSpriteOrientation() * Dir); + //int width = 4; + //if (!isScreenSpace) + //{ + // width = (int)Math.Round(width / cam.Zoom); + //} + //GUI.DrawLine(spriteBatch, startPos, startPos + Vector2.Normalize(up) * size, Color.Red, width: width); + Color color = modifier.DamageMultiplier > 1 ? Color.Red : Color.GreenYellow; float size = ConvertUnits.ToDisplayUnits(body.GetSize().Length() / 2); if (isScreenSpace) { size *= cam.Zoom; } - Color color = modifier.DamageMultiplier > 1 ? Color.Red : Color.GreenYellow; - int width = 4; - if (!isScreenSpace) - { - width = (int)Math.Round(width / cam.Zoom); - } - GUI.DrawLine(spriteBatch, startPos, startPos + Vector2.Normalize(forward) * size, color, width: width); int thickness = 2; if (!isScreenSpace) { thickness = (int)Math.Round(thickness / cam.Zoom); } - ShapeExtensions.DrawSector(spriteBatch, startPos, size, GetArmorSectorSize(modifier.ArmorSectorInRadians) * Dir, 40, color, rotation + MathHelper.Pi, thickness); + float bodyRotation = -body.Rotation; + float constantOffset = -MathHelper.PiOver2; + Vector2 armorSector = modifier.ArmorSectorInRadians; + float armorSectorSize = Math.Abs(armorSector.X - armorSector.Y); + float radians = armorSectorSize * Dir; + float armorSectorOffset = armorSector.X * Dir; + float finalOffset = bodyRotation + constantOffset + armorSectorOffset; + ShapeExtensions.DrawSector(spriteBatch, startPos, size, radians, 40, color, finalOffset, thickness); } } private void DrawWearable(WearableSprite wearable, float depthStep, SpriteBatch spriteBatch, Color color, SpriteEffects spriteEffect) { + var sprite = ActiveSprite; if (wearable.InheritSourceRect) { if (wearable.SheetIndex.HasValue) { - Point location = (ActiveSprite.SourceRect.Location + ActiveSprite.SourceRect.Size) * wearable.SheetIndex.Value; - wearable.Sprite.SourceRect = new Rectangle(location, ActiveSprite.SourceRect.Size); + wearable.Sprite.SourceRect = new Rectangle(CharacterInfo.CalculateOffset(sprite, wearable.SheetIndex.Value), sprite.SourceRect.Size); + } + else if (type == LimbType.Head && character.Info != null && character.Info.Head.SheetIndex.HasValue) + { + wearable.Sprite.SourceRect = new Rectangle(CharacterInfo.CalculateOffset(sprite, character.Info.Head.SheetIndex.Value.ToPoint()), sprite.SourceRect.Size); } else { - wearable.Sprite.SourceRect = ActiveSprite.SourceRect; + wearable.Sprite.SourceRect = sprite.SourceRect; } } Vector2 origin = wearable.Sprite.Origin; if (wearable.InheritOrigin) { - origin = ActiveSprite.Origin; + origin = sprite.Origin; wearable.Sprite.Origin = origin; } else @@ -694,7 +746,7 @@ namespace Barotrauma if (wearable.InheritLimbDepth) { - depth = ActiveSprite.Depth - depthStep; + depth = sprite.Depth - depthStep; Limb depthLimb = (wearable.DepthLimb == LimbType.None) ? this : character.AnimController.GetLimb(wearable.DepthLimb); if (depthLimb != null) { @@ -728,11 +780,11 @@ namespace Barotrauma XElement element; if (random) { - element = info.FilterByTypeAndHeadID(character.Info.FilterElementsByGenderAndRace(character.Info.Wearables), type)?.FirstOrDefault(); + element = info.FilterByTypeAndHeadID(character.Info.FilterElementsByGenderAndRace(character.Info.Wearables), type)?.GetRandom(Rand.RandSync.ClientOnly); } else { - element = info.FilterByTypeAndHeadID(character.Info.FilterElementsByGenderAndRace(character.Info.Wearables), type)?.GetRandom(Rand.RandSync.ClientOnly); + element = info.FilterByTypeAndHeadID(character.Info.FilterElementsByGenderAndRace(character.Info.Wearables), type)?.FirstOrDefault(); } if (element != null) { @@ -749,8 +801,8 @@ namespace Barotrauma DamagedSprite?.Remove(); DamagedSprite = null; - DeformSprite?.Sprite?.Remove(); - DeformSprite = null; + _deformSprite?.Sprite?.Remove(); + _deformSprite = null; DecorativeSprites.ForEach(s => s.Remove()); ConditionalSprites.Clear(); diff --git a/Barotrauma/BarotraumaClient/Source/DebugConsole.cs b/Barotrauma/BarotraumaClient/Source/DebugConsole.cs index 81c38ada9..4acb4a66a 100644 --- a/Barotrauma/BarotraumaClient/Source/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/Source/DebugConsole.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Text; using System.Xml.Linq; using System.Globalization; +using Barotrauma.Extensions; namespace Barotrauma { @@ -56,6 +57,7 @@ namespace Barotrauma private static GUIFrame frame; private static GUIListBox listBox; private static GUITextBox textBox; + private const int maxLength = 1000; public static GUITextBox TextBox => textBox; @@ -92,6 +94,7 @@ namespace Barotrauma { IsFixedSize = false }); + textBox.MaxTextLength = maxLength; textBox.OnKeyHit += (sender, key) => { if (key != Keys.Tab) @@ -109,7 +112,7 @@ namespace Barotrauma } } - public static void Update(GameMain game, float deltaTime) + public static void Update(float deltaTime) { lock (queuedMessages) { @@ -162,6 +165,16 @@ namespace Barotrauma textBox.Text = AutoComplete(textBox.Text, increment: string.IsNullOrEmpty(currentAutoCompletedCommand) ? 0 : 1 ); } + if (PlayerInput.KeyDown(Keys.LeftControl) || PlayerInput.KeyDown(Keys.RightControl)) + { + if ((PlayerInput.KeyDown(Keys.C) || PlayerInput.KeyDown(Keys.D) || PlayerInput.KeyDown(Keys.Z)) && activeQuestionCallback != null) + { + activeQuestionCallback = null; + activeQuestionText = null; + NewMessage(PlayerInput.KeyDown(Keys.C) ? "^C" : PlayerInput.KeyDown(Keys.D) ? "^D" : "^Z", Color.White, true); + } + } + if (PlayerInput.KeyHit(Keys.Enter)) { ExecuteCommand(textBox.Text); @@ -185,41 +198,6 @@ namespace Barotrauma } } - public static void Draw(SpriteBatch spriteBatch) - { - if (!isOpen) return; - - frame.DrawManually(spriteBatch); - } - - private static bool IsCommandPermitted(string command, GameClient client) - { - switch (command) - { - case "kick": - return client.HasPermission(ClientPermissions.Kick); - case "ban": - case "banip": - case "banendpoint": - return client.HasPermission(ClientPermissions.Ban); - case "unban": - case "unbanip": - return client.HasPermission(ClientPermissions.Unban); - case "netstats": - case "help": - case "dumpids": - case "admin": - case "entitylist": - case "togglehud": - case "toggleupperhud": - case "togglecharacternames": - case "fpscounter": - return true; - default: - return client.HasConsoleCommandPermission(command); - } - } - public static void DequeueMessages() { while (queuedMessages.Count > 0) @@ -294,7 +272,7 @@ namespace Barotrauma }; textContainer.RectTransform.NonScaledSize = new Point(textContainer.RectTransform.NonScaledSize.X, textBlock.RectTransform.NonScaledSize.Y + 5); textBlock.SetTextPos(); - var nameBlock = new GUITextBlock(new RectTransform(new Point(150, textContainer.Rect.Height), textContainer.RectTransform), + new GUITextBlock(new RectTransform(new Point(150, textContainer.Rect.Height), textContainer.RectTransform), command.names[0], textAlignment: Alignment.TopLeft); listBox.UpdateScrollBarSize(); @@ -509,14 +487,22 @@ namespace Barotrauma AssignOnExecute("los", (string[] args) => { - GameMain.LightManager.LosEnabled = !GameMain.LightManager.LosEnabled; + if (args.None() || !bool.TryParse(args[0], out bool state)) + { + state = !GameMain.LightManager.LosEnabled; + } + GameMain.LightManager.LosEnabled = state; NewMessage("Line of sight effect " + (GameMain.LightManager.LosEnabled ? "enabled" : "disabled"), Color.White); }); AssignRelayToServer("los", false); AssignOnExecute("lighting|lights", (string[] args) => { - GameMain.LightManager.LightingEnabled = !GameMain.LightManager.LightingEnabled; + if (args.None() || !bool.TryParse(args[0], out bool state)) + { + state = !GameMain.LightManager.LightingEnabled; + } + GameMain.LightManager.LightingEnabled = state; NewMessage("Lighting " + (GameMain.LightManager.LightingEnabled ? "enabled" : "disabled"), Color.White); }); AssignRelayToServer("lighting|lights", false); @@ -635,10 +621,61 @@ namespace Barotrauma } })); + commands.Add(new Command("resetentitiesbyidentifier", "resetentitiesbyidentifier [tag/identifier]: Reset items and structures with the given tag/identifier to prefabs. Only applicable in the subeditor.", args => + { + if (args.Length == 0) { return; } + if (Screen.Selected == GameMain.SubEditorScreen) + { + bool entityFound = false; + foreach (MapEntity entity in MapEntity.mapEntityList) + { + if (entity is Item item) + { + if (item.prefab.Identifier != args[0] && !item.Tags.Contains(args[0])) { continue; } + item.Reset(); + if (MapEntity.SelectedList.Contains(item)) { item.CreateEditingHUD(); } + entityFound = true; + } + else if (entity is Structure structure) + { + if (structure.prefab.Identifier != args[0] && !structure.Tags.Contains(args[0])) { continue; } + structure.Reset(); + if (MapEntity.SelectedList.Contains(structure)) { structure.CreateEditingHUD(); } + entityFound = true; + } + else + { + continue; + } + NewMessage($"Reset {entity.Name}."); + } + if (!entityFound) + { + if (MapEntity.SelectedList.Count == 0) + { + NewMessage("No entities selected."); + return; + } + } + } + }, () => + { + return new string[][] + { + MapEntityPrefab.List.Select(me => me.Identifier).ToArray() + }; + })); + commands.Add(new Command("resetselected", "Reset selected items and structures to prefabs. Only applicable in the subeditor.", args => { if (Screen.Selected == GameMain.SubEditorScreen) { + if (MapEntity.SelectedList.Count == 0) + { + NewMessage("No entities selected."); + return; + } + foreach (MapEntity entity in MapEntity.SelectedList) { if (entity is Item item) @@ -649,6 +686,11 @@ namespace Barotrauma { structure.Reset(); } + else + { + continue; + } + NewMessage($"Reset {entity.Name}."); } foreach (MapEntity entity in MapEntity.SelectedList) { @@ -787,7 +829,11 @@ namespace Barotrauma AssignOnExecute("debugdraw", (string[] args) => { - GameMain.DebugDraw = !GameMain.DebugDraw; + if (args.None() || !bool.TryParse(args[0], out bool state)) + { + state = !GameMain.DebugDraw; + } + GameMain.DebugDraw = state; NewMessage("Debug draw mode " + (GameMain.DebugDraw ? "enabled" : "disabled"), Color.White); }); AssignRelayToServer("debugdraw", false); @@ -1012,22 +1058,38 @@ namespace Barotrauma commands.Add(new Command("checkmissingloca", "", (string[] args) => { - foreach (MapEntityPrefab me in MapEntityPrefab.List) + //key = text tag, value = list of languages the tag is missing from + Dictionary> missingTags = new Dictionary>(); + Dictionary> tags = new Dictionary>(); + foreach (string language in TextManager.AvailableLanguages) { - string name = TextManager.Get("entityname." + me.Identifier, returnNull: true); - if (!string.IsNullOrEmpty(name)) { continue; } + TextManager.Language = language; + tags.Add(language, new HashSet(TextManager.GetAllTagTextPairs().Select(t => t.Key))); + } - if (me is ItemPrefab itemPrefab) + foreach (string englishTag in tags["English"]) + { + if (englishTag == "entitydescription.reinforceddoor") { - string nameIdentifier = itemPrefab.ConfigElement?.GetAttributeString("nameidentifier", ""); - if (nameIdentifier != null) + int asdfsdf = 1; + } + foreach (string language in TextManager.AvailableLanguages) + { + if (language == "English") { continue; } + if (!tags[language].Contains(englishTag)) { - name = TextManager.Get("entityname." + nameIdentifier, returnNull: true); - if (!string.IsNullOrEmpty(name)) { continue; } + if (!missingTags.ContainsKey(englishTag)) + { + missingTags[englishTag] = new List(); + } + missingTags[englishTag].Add(language); } } - NewMessage("Entity name not translated (" + me.Name + ", " + me.Identifier + ")!", me is ItemPrefab ? Color.Red : Color.Yellow); } + string filePath = "missingloca.txt"; + File.WriteAllLines(filePath, missingTags.Select(t => "\""+t.Key + "\"\n missing from " + string.Join(", ", t.Value))); + System.Diagnostics.Process.Start(Path.GetFullPath(filePath)); + TextManager.Language = "English"; })); commands.Add(new Command("spamchatmessages", "", (string[] args) => @@ -1107,21 +1169,32 @@ namespace Barotrauma { if (!structure.ResizeHorizontal) { - structure.Rect = new Rectangle(structure.Rect.X, structure.Rect.Y, + structure.Rect = structure.DefaultRect = 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 = structure.DefaultRect = new Rectangle(structure.Rect.X, structure.Rect.Y, structure.Rect.Width, (int)structure.Prefab.ScaledSize.Y); } + } } } } }, isCheat: false)); + + commands.Add(new Command("flip", "Flip the currently controlled character.", (string[] args) => + { + Character.Controlled?.AnimController.Flip(); + }, isCheat: false)); + commands.Add(new Command("mirror", "Mirror the currently controlled character.", (string[] args) => + { + (Character.Controlled?.AnimController as FishAnimController)?.Mirror(lerp: false); + }, 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) => @@ -1237,21 +1310,53 @@ namespace Barotrauma { Dictionary typeNames = new Dictionary { - { "Single", "float"}, - { "Int32", "integer"}, - { "Boolean", "true/false"}, - { "String", "text"}, + { "Single", "Float"}, + { "Int32", "Integer"}, + { "Boolean", "True/False"}, + { "String", "Text"}, }; - var itemComponentTypes = typeof(ItemComponent).Assembly.GetTypes().Where(type => type.IsSubclassOf(typeof(ItemComponent))); + var itemComponentTypes = typeof(ItemComponent).Assembly.GetTypes().Where(type => type.IsSubclassOf(typeof(ItemComponent))).ToList(); + itemComponentTypes.Sort((i1, i2) => { return i1.Name.CompareTo(i2.Name); }); + + itemComponentTypes.Insert(0, typeof(ItemComponent)); + string filePath = args.Length > 0 ? args[0] : "ItemComponentDocumentation.txt"; List lines = new List(); foreach (Type t in itemComponentTypes) { - lines.Add($"[b]{t.Name}[/b]"); + + lines.Add($"[h1]{t.Name}[/h1]"); lines.Add(""); - var properties = t.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.DeclaredOnly);//.Cast(); + var properties = t.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.DeclaredOnly).ToList();//.Cast(); + Type baseType = t.BaseType; + while (baseType != null && baseType != typeof(ItemComponent)) + { + properties.AddRange(baseType.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.DeclaredOnly)); + baseType = baseType.BaseType; + } + + if (!properties.Any(p => p.GetCustomAttributes(true).Any(a => a is Serialize))) + { + lines.Add("No editable properties."); + lines.Add(""); + continue; + } + + lines.Add("[table]"); + lines.Add(" [tr]"); + + lines.Add(" [th]Name[/th]"); + lines.Add(" [th]Type[/th]"); + lines.Add(" [th]Default value[/th]"); + //lines.Add(" [th]Range[/th]"); + lines.Add(" [th]Description[/th]"); + + lines.Add(" [/tr]"); + + + Dictionary dictionary = new Dictionary(); foreach (var property in properties) { @@ -1273,29 +1378,41 @@ namespace Barotrauma } propertyTypeName = string.Join("/", valueNames); } - - lines.Add($"{property.Name} ({propertyTypeName})"); - - if (!string.IsNullOrEmpty(serialize.Description)) + string defaultValueString = serialize.defaultValue?.ToString() ?? ""; + if (property.PropertyType == typeof(float)) { - lines.Add(serialize.Description); + defaultValueString = ((float)serialize.defaultValue).ToString(CultureInfo.InvariantCulture); } + + lines.Add(" [tr]"); + + lines.Add($" [td]{property.Name}[/td]"); + lines.Add($" [td]{propertyTypeName}[/td]"); + lines.Add($" [td]{defaultValueString}[/td]"); + Editable editable = attributes.FirstOrDefault(a => a is Editable) as Editable; + string rangeText = "-"; if (editable != null) { if (editable.MinValueFloat > float.MinValue || editable.MaxValueFloat < float.MaxValue) { - lines.Add("Range: " + editable.MinValueFloat+"-"+editable.MaxValueFloat); + rangeText = editable.MinValueFloat + "-" + editable.MaxValueFloat; } else if (editable.MinValueInt > int.MinValue || editable.MaxValueInt < int.MaxValue) { - lines.Add("Range: " + editable.MinValueInt + "-" + editable.MaxValueInt); + rangeText = editable.MinValueInt + "-" + editable.MaxValueInt; } } + //lines.Add($" [td]{rangeText}[/td]"); - lines.Add("Default value: " + serialize.defaultValue); - lines.Add(""); + if (!string.IsNullOrEmpty(serialize.Description)) + { + lines.Add($" [td]{serialize.Description}[/td]"); + } + + lines.Add(" [/tr]"); } + lines.Add("[/table]"); lines.Add(""); } File.WriteAllLines(filePath, lines); @@ -1546,10 +1663,12 @@ namespace Barotrauma (string[] args) => { if (GameMain.Client == null || args.Length == 0) return; - ShowQuestionPrompt("Reason for banning the endpoint \"" + args[0] + "\"?", (reason) => + ShowQuestionPrompt("Reason for banning the endpoint \"" + args[0] + "\"? (Enter c to cancel)", (reason) => { - ShowQuestionPrompt("Enter the duration of the ban (leave empty to ban permanently, or use the format \"[days] d [hours] h\")", (duration) => + if (reason == "c" || reason == "C") { return; } + ShowQuestionPrompt("Enter the duration of the ban (leave empty to ban permanently, or use the format \"[days] d [hours] h\") (Enter c to cancel)", (duration) => { + if (duration == "c" || duration == "C") { return; } TimeSpan? banDuration = null; if (!string.IsNullOrWhiteSpace(duration)) { @@ -1866,10 +1985,17 @@ namespace Barotrauma { character.Info.Race = race; character.ReloadHead(); + foreach (var limb in character.AnimController.Limbs) + { + if (limb.type != LimbType.Head) + { + limb.RecreateSprites(); + } + } } }, isCheat: true)); - commands.Add(new Command("loadhead|head", "Load head sprite(s). Required argument: head id. Optional arguments: hair index, beard index, moustache index, face attachment index.", args => + commands.Add(new Command("head", "Load the head sprite and the wearables (hair etc). Required argument: head id. Optional arguments: hair index, beard index, moustache index, face attachment index.", args => { var character = Character.Controlled; if (character == null) @@ -1903,6 +2029,13 @@ namespace Barotrauma int.TryParse(args[4], out faceAttachmentIndex); } character.ReloadHead(id, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); + foreach (var limb in character.AnimController.Limbs) + { + if (limb.type != LimbType.Head) + { + limb.RecreateSprites(); + } + } } }, isCheat: true)); @@ -1948,13 +2081,13 @@ namespace Barotrauma { wearable.Variant = variant; } - wearable.RefreshPath(); + wearable.ParsePath(true); wearable.Sprite.ReloadXML(); wearable.Sprite.ReloadTexture(); } foreach (var wearable in limb.OtherWearables) { - wearable.RefreshPath(); + wearable.ParsePath(true); wearable.Sprite.ReloadXML(); wearable.Sprite.ReloadTexture(); } diff --git a/Barotrauma/BarotraumaClient/Source/Events/Missions/Mission.cs b/Barotrauma/BarotraumaClient/Source/Events/Missions/Mission.cs index 907f11bb5..6d839a784 100644 --- a/Barotrauma/BarotraumaClient/Source/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaClient/Source/Events/Missions/Mission.cs @@ -1,4 +1,6 @@ -namespace Barotrauma +using Barotrauma.Networking; + +namespace Barotrauma { partial class Mission { @@ -14,5 +16,10 @@ IconColor = Prefab.IconColor }; } + + public void ClientRead(IReadMessage msg) + { + State = msg.ReadInt16(); + } } } diff --git a/Barotrauma/BarotraumaClient/Source/GUI/ChatBox.cs b/Barotrauma/BarotraumaClient/Source/GUI/ChatBox.cs index b4c7d5308..b93ad97c1 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/ChatBox.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/ChatBox.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using Barotrauma.Networking; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; @@ -52,6 +53,8 @@ namespace Barotrauma public GUIButton ToggleButton { get; private set; } + private GUIButton showNewMessagesButton; + public ChatBox(GUIComponent parent, bool isSinglePlayer) { this.IsSinglePlayer = isSinglePlayer; @@ -65,8 +68,9 @@ namespace Barotrauma int toggleButtonWidth = (int)(30 * GUI.Scale); GUIFrame = new GUIFrame(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.ChatBoxArea, parent.RectTransform), style: null); - var chatBoxHolder = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), GUIFrame.RectTransform), style: "ChatBox"); + var chatBoxHolder = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.875f), GUIFrame.RectTransform), style: "ChatBox"); chatBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.95f), chatBoxHolder.RectTransform, Anchor.CenterRight), style: null); + ToggleButton = new GUIButton(new RectTransform(new Point(toggleButtonWidth, HUDLayoutSettings.ChatBoxArea.Height), parent.RectTransform), style: "UIToggleButton"); @@ -76,7 +80,7 @@ namespace Barotrauma return true; }; - InputBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.1f), GUIFrame.RectTransform, Anchor.BottomCenter), + InputBox = new GUITextBox(new RectTransform(new Vector2(0.925f, 0.125f), GUIFrame.RectTransform, Anchor.BottomLeft), style: "ChatTextBox") { Font = GUI.SmallFont, @@ -84,9 +88,25 @@ namespace Barotrauma }; InputBox.OnDeselected += (gui, Keys) => { - gui.Text = ""; + //gui.Text = ""; }; - + + var chatSendButton = new GUIButton(new RectTransform(new Vector2(0.075f, 0.125f), GUIFrame.RectTransform, Anchor.BottomRight) { RelativeOffset = new Vector2(0.0f, -0.01f) }, ">"); + chatSendButton.OnClicked += (GUIButton btn, object userdata) => + { + InputBox.OnEnterPressed(InputBox, InputBox.Text); + return true; + }; + + showNewMessagesButton = new GUIButton(new RectTransform(new Vector2(1f, 0.125f), GUIFrame.RectTransform, Anchor.BottomCenter) { RelativeOffset = new Vector2(0.0f, -0.125f) }, TextManager.Get("chat.shownewmessages")); + showNewMessagesButton.OnClicked += (GUIButton btn, object userdata) => + { + chatBox.ScrollBar.BarScrollValue = 1f; + showNewMessagesButton.Visible = false; + return true; + }; + + showNewMessagesButton.Visible = false; ToggleOpen = GameMain.Config.ChatOpen; } @@ -133,7 +153,7 @@ namespace Barotrauma public void AddMessage(ChatMessage message) { - while (chatBox.Content.CountChildren > 20) + while (chatBox.Content.CountChildren > 60) { chatBox.RemoveChild(chatBox.Content.Children.First()); } @@ -155,18 +175,21 @@ namespace Barotrauma var msgHolder = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.0f), chatBox.Content.RectTransform, Anchor.TopCenter), style: null, color: ((chatBox.Content.CountChildren % 2) == 0) ? Color.Transparent : Color.Black * 0.1f); - GUITextBlock senderNameBlock = null; + GUITextBlock senderNameBlock = new GUITextBlock(new RectTransform(new Vector2(0.98f, 0.0f), msgHolder.RectTransform) { AbsoluteOffset = new Point((int)(5 * GUI.Scale), 0) }, + ChatMessage.GetTimeStamp(), textColor: Color.LightGray, font: GUI.SmallFont, textAlignment: Alignment.TopLeft, style: null) + { + CanBeFocused = true + }; if (!string.IsNullOrEmpty(senderName)) { - senderNameBlock = new GUITextBlock(new RectTransform(new Vector2(0.98f, 0.0f), msgHolder.RectTransform) - { AbsoluteOffset = new Point((int)(5 * GUI.Scale), 0) }, + new GUITextBlock(new RectTransform(new Vector2(0.8f, 1.0f), senderNameBlock.RectTransform) { AbsoluteOffset = new Point((int)(senderNameBlock.TextSize.X), 0) }, senderName, textColor: senderColor, font: GUI.SmallFont, textAlignment: Alignment.TopLeft, style: null) { CanBeFocused = true }; } - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), msgHolder.RectTransform) + var msgText =new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), msgHolder.RectTransform) { AbsoluteOffset = new Point((int)(10 * GUI.Scale), senderNameBlock == null ? 0 : senderNameBlock.Rect.Height) }, displayedText, textColor: message.Color, font: GUI.SmallFont, textAlignment: Alignment.TopLeft, style: null, wrap: true, color: ((chatBox.Content.CountChildren % 2) == 0) ? Color.Transparent : Color.Black * 0.1f) @@ -185,12 +208,28 @@ namespace Barotrauma { msgHolder.Flash(Color.Yellow * 0.6f); } - //resize the holder to match the size of the message and add some spacing - msgHolder.RectTransform.Resize(new Point(msgHolder.Rect.Width, msgHolder.Children.Sum(c => c.Rect.Height) + (int)(10 * GUI.Scale)), resizeChildren: false); + msgHolder.RectTransform.SizeChanged += Recalculate; + Recalculate(); + void Recalculate() + { + msgHolder.RectTransform.SizeChanged -= Recalculate; + //resize the holder to match the size of the message and add some spacing + msgText.RectTransform.MaxSize = new Point(msgHolder.Rect.Width - msgText.RectTransform.AbsoluteOffset.X, int.MaxValue); + senderNameBlock.RectTransform.MaxSize = new Point(msgHolder.Rect.Width - senderNameBlock.RectTransform.AbsoluteOffset.X, int.MaxValue); + msgHolder.Children.ForEach(c => (c as GUITextBlock)?.CalculateHeightFromText()); + msgHolder.RectTransform.Resize(new Point(msgHolder.Rect.Width, msgHolder.Children.Sum(c => c.Rect.Height) + (int)(10 * GUI.Scale)), resizeChildren: false); + msgHolder.RectTransform.SizeChanged += Recalculate; + chatBox.RecalculateChildren(); + } CoroutineManager.StartCoroutine(UpdateMessageAnimation(msgHolder, 0.5f)); chatBox.UpdateScrollBarSize(); + + if (chatBox.ScrollBar.Visible && chatBox.ScrollBar.BarScroll < 1f) + { + showNewMessagesButton.Visible = true; + } if (!ToggleOpen) { @@ -203,16 +242,16 @@ namespace Barotrauma { CanBeFocused = false }; - var msgText = new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.0f), popupMsg.RectTransform, Anchor.TopRight) + var msgPopupText = new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.0f), popupMsg.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point(0, senderText.Rect.Height) }, displayedText, textColor: message.Color, font: GUI.SmallFont, textAlignment: Alignment.TopRight, style: null, wrap: true) { CanBeFocused = false }; int textWidth = (int)Math.Max( - msgText.Font.MeasureString(msgText.WrappedText).X, + msgPopupText.Font.MeasureString(msgPopupText.WrappedText).X, senderText.Font.MeasureString(senderText.WrappedText).X); - popupMsg.RectTransform.Resize(new Point(textWidth + 20, msgText.Rect.Bottom - senderText.Rect.Y), resizeChildren: false); + popupMsg.RectTransform.Resize(new Point(textWidth + 20, msgPopupText.Rect.Bottom - senderText.Rect.Y), resizeChildren: false); popupMessages.Enqueue(popupMsg); } @@ -277,6 +316,11 @@ namespace Barotrauma prevUIScale = GUI.Scale; } + if (showNewMessagesButton.Visible && chatBox.ScrollBar.BarScroll == 1f) + { + showNewMessagesButton.Visible = false; + } + if (ToggleOpen || (InputBox != null && InputBox.Selected)) { openState += deltaTime * 5.0f; diff --git a/Barotrauma/BarotraumaClient/Source/GUI/ComponentStyle.cs b/Barotrauma/BarotraumaClient/Source/GUI/ComponentStyle.cs index 565460059..09d15b36d 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/ComponentStyle.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/ComponentStyle.cs @@ -19,12 +19,16 @@ namespace Barotrauma public readonly Color OutlineColor; + public readonly XElement Element; + public readonly Dictionary> Sprites; public Dictionary ChildStyles; public GUIComponentStyle(XElement element) { + Element = element; + Sprites = new Dictionary>(); foreach (GUIComponent.ComponentState state in Enum.GetValues(typeof(GUIComponent.ComponentState))) { diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUI.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUI.cs index c87231d3d..19b88d3cd 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUI.cs @@ -29,6 +29,35 @@ namespace Barotrauma { public static GUICanvas Canvas => GUICanvas.Instance; + public static readonly SamplerState SamplerState = new SamplerState() + { + Filter = TextureFilter.Linear, + AddressU = TextureAddressMode.Wrap, + AddressV = TextureAddressMode.Wrap, + AddressW = TextureAddressMode.Wrap, + BorderColor = Color.White, + MaxAnisotropy = 4, + MaxMipLevel = 0, + MipMapLevelOfDetailBias = -0.8f, + ComparisonFunction = CompareFunction.Never, + FilterMode = TextureFilterMode.Default, + }; + + public static readonly SamplerState SamplerStateClamp = new SamplerState() + { + Filter = TextureFilter.Linear, + AddressU = TextureAddressMode.Clamp, + AddressV = TextureAddressMode.Clamp, + AddressW = TextureAddressMode.Clamp, + BorderColor = Color.White, + MaxAnisotropy = 4, + MaxMipLevel = 0, + MipMapLevelOfDetailBias = -0.8f, + ComparisonFunction = CompareFunction.Never, + FilterMode = TextureFilterMode.Default, + }; + + public static readonly string[] vectorComponentLabels = { "X", "Y", "Z", "W" }; public static readonly string[] rectComponentLabels = { "X", "Y", "W", "H" }; public static readonly string[] colorComponentLabels = { "R", "G", "B", "A" }; @@ -455,7 +484,11 @@ namespace Barotrauma if (GameMain.WindowActive) { + spriteBatch.End(); + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerStateClamp, rasterizerState: GameMain.ScissorTestEnable); Cursor.Draw(spriteBatch, PlayerInput.LatestMousePosition, 0, Scale / 2f); + spriteBatch.End(); + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); } } @@ -664,6 +697,7 @@ namespace Barotrauma /// public static GUIComponent UpdateMouseOn() { + GUIComponent prevMouseOn = MouseOn; MouseOn = null; int inventoryIndex = -1; if (Inventory.IsMouseOnInventory()) @@ -673,9 +707,13 @@ namespace Barotrauma for (int i = updateList.Count - 1; i > inventoryIndex; i--) { GUIComponent c = updateList[i]; + if (!c.CanBeFocused) { continue; } if (c.MouseRect.Contains(PlayerInput.MousePosition)) { - MouseOn = c; + if ((!PlayerInput.LeftButtonHeld() && !PlayerInput.LeftButtonClicked()) || c == prevMouseOn) + { + MouseOn = c; + } break; } } @@ -1629,42 +1667,9 @@ namespace Barotrauma return true; } - private static bool QuitClicked(GUIButton button, object obj) + public static bool QuitClicked(GUIButton button, object obj) { - bool save = button.UserData as string == "save"; - if (save) - { - SaveUtil.SaveGame(GameMain.GameSession.SavePath); - } - - if (GameMain.Client != null) - { - GameMain.Client.Disconnect(); - GameMain.Client = null; - } - - CoroutineManager.StopCoroutines("EndCinematic"); - - if (GameMain.GameSession != null) - { - if (Tutorial.Initialized) - { - ((TutorialMode)GameMain.GameSession.GameMode).Tutorial.Stop(); - } - - if (GameSettings.SendUserStatistics) - { - Mission mission = GameMain.GameSession.Mission; - GameAnalyticsManager.AddDesignEvent("QuitRound:" + (save ? "Save" : "NoSave")); - GameAnalyticsManager.AddDesignEvent("EndRound:" + (mission == null ? "NoMission" : (mission.Completed ? "MissionCompleted" : "MissionFailed"))); - } - GameMain.GameSession = null; - } - - GUIMessageBox.CloseAll(); - - GameMain.MainMenuScreen.Select(); - + GameMain.QuitToMainMenu(button.UserData as string == "save"); return true; } diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUIButton.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUIButton.cs index 79282e4bd..bdb6b4627 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUIButton.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUIButton.cs @@ -153,15 +153,18 @@ namespace Barotrauma public GUIButton(RectTransform rectT, string text = "", Alignment textAlignment = Alignment.Center, string style = "", Color? color = null) : base(style, rectT) { + CanBeFocused = true; + if (color.HasValue) { this.color = color.Value; } - frame = new GUIFrame(new RectTransform(Vector2.One, rectT), style); + frame = new GUIFrame(new RectTransform(Vector2.One, rectT), style) { CanBeFocused = false }; if (style != null) GUI.Style.Apply(frame, style == "" ? "GUIButton" : style); textBlock = new GUITextBlock(new RectTransform(Vector2.One, rectT), text, textAlignment: textAlignment, style: null) { - TextColor = this.style == null ? Color.Black : this.style.textColor + TextColor = this.style == null ? Color.Black : this.style.textColor, + CanBeFocused = false }; if (rectT.Rect.Height == 0 && !string.IsNullOrEmpty(text)) { diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUIComponent.cs index cad4abb24..cb6ed0528 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUIComponent.cs @@ -106,11 +106,21 @@ namespace Barotrauma return Children.Where(c => c.userData == userData); } + public IEnumerable FindChildren(Func predicate) + { + return Children.Where(c => predicate(c)); + } + public virtual void ClearChildren() { RectTransform.ClearChildren(); } + public void SetAsFirstChild() + { + RectTransform.SetAsFirstChild(); + } + public void SetAsLastChild() { RectTransform.SetAsLastChild(); @@ -319,7 +329,7 @@ namespace Barotrauma Font = GUI.Font; - CanBeFocused = true; + CanBeFocused = true; //TODO: change default to false? if (style != null) GUI.Style.Apply(this, style); @@ -729,9 +739,10 @@ namespace Barotrauma private static GUITextBlock LoadGUITextBlock(XElement element, RectTransform parent, string overrideText = null, Anchor? anchor = null) { - string text = element.Attribute("text") == null ? - element.ElementInnerText() : - element.GetAttributeString("text", ""); + string text = overrideText ?? + (element.Attribute("text") == null ? + element.ElementInnerText() : + element.GetAttributeString("text", "")); text = text.Replace(@"\n", "\n"); string style = element.GetAttributeString("style", ""); diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUICustomComponent.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUICustomComponent.cs index 2fd27cc60..6289aba4e 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUICustomComponent.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUICustomComponent.cs @@ -29,7 +29,7 @@ namespace Barotrauma { spriteBatch.End(); spriteBatch.GraphicsDevice.ScissorRectangle = Rectangle.Intersect(prevScissorRect, Rect); - spriteBatch.Begin(SpriteSortMode.Deferred, rasterizerState: GameMain.ScissorTestEnable); + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); } OnDraw?.Invoke(spriteBatch, this); @@ -38,7 +38,7 @@ namespace Barotrauma { spriteBatch.End(); spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect; - spriteBatch.Begin(SpriteSortMode.Deferred, rasterizerState: GameMain.ScissorTestEnable); + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); } } diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUIDropDown.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUIDropDown.cs index 4ff2c0079..151b31ec9 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUIDropDown.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUIDropDown.cs @@ -16,7 +16,7 @@ namespace Barotrauma private GUIButton button; private GUIListBox listBox; - private RectTransform currentListBoxParent; + private RectTransform currentHighestParent; private List parentHierarchy = new List(); private bool selectMultiple; @@ -141,6 +141,8 @@ namespace Barotrauma public GUIDropDown(RectTransform rectT, string text = "", int elementCount = 4, string style = "", bool selectMultiple = false) : base(style, rectT) { + CanBeFocused = true; + this.selectMultiple = selectMultiple; button = new GUIButton(new RectTransform(Vector2.One, rectT), text, Alignment.CenterLeft, style: "GUIDropDown") @@ -149,7 +151,7 @@ namespace Barotrauma }; GUI.Style.Apply(button, "", this); - listBox = new GUIListBox(new RectTransform(new Point(Rect.Width, Rect.Height * MathHelper.Clamp(elementCount, 2, 10)), rectT, Anchor.BottomLeft, Pivot.TopLeft) + listBox = new GUIListBox(new RectTransform(new Point(Rect.Width, Rect.Height * MathHelper.Clamp(elementCount, 2, 10)), rectT, Anchor.BottomCenter, Pivot.TopCenter) { IsFixedSize = false }, style: null) { Enabled = !selectMultiple, @@ -157,48 +159,55 @@ namespace Barotrauma }; GUI.Style.Apply(listBox.Content, "GUIListBox", this); - currentListBoxParent = FindListBoxParent(); - currentListBoxParent.GUIComponent.OnAddedToGUIUpdateList += AddListBoxToGUIUpdateList; + currentHighestParent = FindHighestParent(); + currentHighestParent.GUIComponent.OnAddedToGUIUpdateList += AddListBoxToGUIUpdateList; rectT.ParentChanged += (RectTransform newParent) => { - currentListBoxParent.GUIComponent.OnAddedToGUIUpdateList -= AddListBoxToGUIUpdateList; + currentHighestParent.GUIComponent.OnAddedToGUIUpdateList -= AddListBoxToGUIUpdateList; if (newParent != null) { - currentListBoxParent = FindListBoxParent(); - currentListBoxParent.GUIComponent.OnAddedToGUIUpdateList += AddListBoxToGUIUpdateList; + currentHighestParent = FindHighestParent(); + currentHighestParent.GUIComponent.OnAddedToGUIUpdateList += AddListBoxToGUIUpdateList; } }; } /// - /// Finds the component after which the listbox should be drawn. Usually the parent of the dropdown, but if the dropdown - /// is the child of another GUIListBox, we need to draw our listbox after that because listboxes clip everything outside their rect. + /// Finds the component after which the listbox should be drawn + /// //(= the component highest in the hierarchy, to get the listbox + /// //to be rendered on top of all of it's children) /// - private RectTransform FindListBoxParent() + private RectTransform FindHighestParent() { parentHierarchy.Clear(); + + //collect entire parent hierarchy to a list parentHierarchy = new List() { RectTransform.Parent }; - while (parentHierarchy.Last().Parent != null) + RectTransform parent = parentHierarchy.Last(); + while (parent?.Parent != null) { - parentHierarchy.Add(parentHierarchy.Last().Parent); + parentHierarchy.Add(parent.Parent); + parent = parent.Parent; } - //find the parent GUIListBox highest in the hierarchy - for (int i = parentHierarchy.Count - 1; i >= 0; i--) + + //find the highest parent that has a guicomponent with a style + //(and so should be rendered and not just some empty parent/root element used for constructing a layout) + for (int i = parentHierarchy.Count - 1; i > 0; i--) { - if (parentHierarchy[i].GUIComponent is GUIListBox) + if (parentHierarchy[i] is GUICanvas || + parentHierarchy[i].GUIComponent == null || + parentHierarchy[i].GUIComponent.Style == null || + parentHierarchy[i].GUIComponent == Screen.Selected?.Frame) { - if (parentHierarchy[i].Parent != null && parentHierarchy[i].Parent.GUIComponent != null) - { - return parentHierarchy[i].Parent; - } - return parentHierarchy[i]; + parentHierarchy.RemoveAt(i); + } + else + { + break; } } - //or just go with the direct parent if there are no listboxes in the hierarchy - parentHierarchy.Clear(); - parentHierarchy.Add(RectTransform.Parent); - return RectTransform.Parent; + return parentHierarchy.Last(); } public void AddItem(string text, object userData = null, string toolTip = "") diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUIFrame.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUIFrame.cs index eccf78a2f..582eb986f 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUIFrame.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUIFrame.cs @@ -20,7 +20,7 @@ namespace Barotrauma Color currColor = GetCurrentColor(state); - if (sprites == null || !sprites.Any()) GUI.DrawRectangle(spriteBatch, Rect, currColor * (currColor.A/255.0f), true); + if (sprites == null || !sprites.Any(s => s.Value.Any())) GUI.DrawRectangle(spriteBatch, Rect, currColor * (currColor.A/255.0f), true); base.Draw(spriteBatch); if (OutlineColor != Color.Transparent) diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUIImage.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUIImage.cs index 9817060fd..1f1d54575 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUIImage.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUIImage.cs @@ -1,6 +1,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; +using System.Linq; namespace Barotrauma { @@ -57,6 +58,10 @@ namespace Barotrauma } } + public BlendState BlendState; + + public ComponentState? OverrideState = null; + public GUIImage(RectTransform rectT, string style, bool scaleToFit = false) : this(rectT, null, null, scaleToFit, style) { @@ -82,9 +87,7 @@ namespace Barotrauma } if (style == null) { - color = Color.White; - hoverColor = Color.White; - selectedColor = Color.White; + color = hoverColor = selectedColor = pressedColor = Color.White; } if (!scaleToFit) { @@ -99,7 +102,17 @@ namespace Barotrauma protected override void Draw(SpriteBatch spriteBatch) { if (!Visible) return; + + if (Parent != null) { state = Parent.State; } + if (OverrideState != null) { state = OverrideState.Value; } Color currColor = GetCurrentColor(state); + + if (BlendState != null) + { + spriteBatch.End(); + spriteBatch.Begin(blendState: BlendState, samplerState: GUI.SamplerState); + } + if (style != null) { foreach (UISprite uiSprite in style.Sprites[state]) @@ -121,6 +134,12 @@ namespace Barotrauma spriteBatch.Draw(sprite.Texture, Rect.Center.ToVector2(), sourceRect, currColor * (currColor.A / 255.0f), Rotation, sprite.size / 2, Scale, SpriteEffects, 0.0f); } + + if (BlendState != null) + { + spriteBatch.End(); + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); + } } private void RecalculateScale() diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUILayoutGroup.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUILayoutGroup.cs index e797b022a..96bc0edd8 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUILayoutGroup.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUILayoutGroup.cs @@ -1,4 +1,5 @@ using Microsoft.Xna.Framework; +using System; using System.Linq; namespace Barotrauma @@ -71,6 +72,8 @@ namespace Barotrauma public GUILayoutGroup(RectTransform rectT, bool isHorizontal = false, Anchor childAnchor = Anchor.TopLeft) : base(null, rectT) { + CanBeFocused = false; + this.isHorizontal = isHorizontal; this.childAnchor = childAnchor; rectT.ChildrenChanged += (child) => needsToRecalculate = true; @@ -83,17 +86,25 @@ namespace Barotrauma float stretchFactor = 1.0f; if (stretch && RectTransform.Children.Count() > 0) { + float minSize = RectTransform.Children + .Where(c => !c.GUIComponent.IgnoreLayoutGroups) + .Sum(c => isHorizontal ? c.MinSize.X : c.MinSize.Y); + float totalSize = RectTransform.Children .Where(c => !c.GUIComponent.IgnoreLayoutGroups) .Sum(c => isHorizontal ? MathHelper.Clamp(c.Rect.Width, c.MinSize.X, c.MaxSize.X) : MathHelper.Clamp(c.Rect.Height, c.MinSize.Y, c.MaxSize.Y)); + float thisSize = (isHorizontal ? Rect.Width : Rect.Height); + totalSize += (RectTransform.Children.Count() - 1) * - (absoluteSpacing + relativeSpacing * (isHorizontal ? Rect.Width : Rect.Height)); + (absoluteSpacing + relativeSpacing * thisSize); - stretchFactor = totalSize <= 0.0f ? 1.0f : (isHorizontal ? Rect.Width: Rect.Height) / totalSize; + stretchFactor = totalSize <= 0.0f || minSize >= thisSize ? + 1.0f : + (thisSize - minSize) / (totalSize - minSize); } int absPos = 0; @@ -106,7 +117,7 @@ namespace Barotrauma { child.RelativeOffset = new Vector2(relPos, child.RelativeOffset.Y); child.AbsoluteOffset = new Point(absPos, child.AbsoluteOffset.Y); - absPos += (int)((child.Rect.Width + absoluteSpacing) * stretchFactor); + absPos += (int)Math.Max((child.Rect.Width + absoluteSpacing) * stretchFactor, child.MinSize.X); if (stretch) { child.RelativeSize = new Vector2(child.RelativeSize.X * stretchFactor, child.RelativeSize.Y); @@ -116,7 +127,7 @@ namespace Barotrauma { child.RelativeOffset = new Vector2(child.RelativeOffset.X, relPos); child.AbsoluteOffset = new Point(child.AbsoluteOffset.X, absPos); - absPos += (int)((child.Rect.Height + absoluteSpacing) * stretchFactor); + absPos += (int)Math.Max((child.Rect.Height + absoluteSpacing) * stretchFactor, child.MinSize.Y); if (stretch) { child.RelativeSize = new Vector2(child.RelativeSize.X, child.RelativeSize.Y * stretchFactor); diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUIListBox.cs index ed822aa59..586663afe 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUIListBox.cs @@ -135,6 +135,8 @@ namespace Barotrauma public GUIListBox(RectTransform rectT, bool isHorizontal = false, Color? color = null, string style = "") : base(style, rectT) { + CanBeFocused = true; + selected = new List(); Point frameSize = isHorizontal ? @@ -176,7 +178,7 @@ namespace Barotrauma private void UpdateDimensions() { - if (!ScrollBarEnabled) + if (!ScrollBar.Visible) { Content.RectTransform.NonScaledSize = Rect.Size; } @@ -295,7 +297,7 @@ namespace Barotrauma if (child == null) continue; // selecting - if (Enabled && child.CanBeFocused && (GUI.IsMouseOn(child)) && child.Rect.Contains(PlayerInput.MousePosition)) + if (Enabled && CanBeFocused && child.CanBeFocused && (GUI.IsMouseOn(child)) && child.Rect.Contains(PlayerInput.MousePosition)) { child.State = ComponentState.Hover; if (PlayerInput.LeftButtonClicked()) @@ -322,6 +324,15 @@ namespace Barotrauma public override void AddToGUIUpdateList(bool ignoreChildren = false, int order = 0) { if (!Visible) { return; } + + if (!ignoreChildren) + { + foreach (GUIComponent child in Children) + { + if (child == Content || child == ScrollBar) { continue; } + child.AddToGUIUpdateList(ignoreChildren, order); + } + } foreach (GUIComponent child in Content.Children) { @@ -406,12 +417,16 @@ namespace Barotrauma scrollBarNeedsRecalculation = false; } + bool prevScrollBarVisible = ScrollBar.Visible; + ScrollBar.Enabled = ScrollBarEnabled && ScrollBar.BarSize < 1.0f; if (AutoHideScrollBar) { ScrollBar.Visible = ScrollBar.BarSize < 1.0f; } + if (ScrollBar.Visible != prevScrollBarVisible) { UpdateDimensions(); } + if ((GUI.IsMouseOn(this) || GUI.IsMouseOn(ScrollBar)) && PlayerInput.ScrollWheelSpeed != 0) { ScrollBar.BarScroll -= (PlayerInput.ScrollWheelSpeed / 500.0f) * BarSize; @@ -594,7 +609,7 @@ namespace Barotrauma { spriteBatch.End(); spriteBatch.GraphicsDevice.ScissorRectangle = Rectangle.Intersect(prevScissorRect, Content.Rect); - spriteBatch.Begin(SpriteSortMode.Deferred, rasterizerState: GameMain.ScissorTestEnable); + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); } var children = Content.Children; @@ -618,7 +633,7 @@ namespace Barotrauma { spriteBatch.End(); spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect; - spriteBatch.Begin(SpriteSortMode.Deferred, rasterizerState: prevRasterizerState); + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: prevRasterizerState); } if (ScrollBar.Visible) ScrollBar.DrawManually(spriteBatch, alsoChildren: true, recursive: true); diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUINumberInput.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUINumberInput.cs index ca436e624..b456de7ec 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUINumberInput.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUINumberInput.cs @@ -119,6 +119,19 @@ namespace Barotrauma } } + public override ScalableFont Font + { + get + { + return base.Font; + } + set + { + base.Font = value; + if (TextBox != null) { TextBox.Font = value; } + } + } + public GUILayoutGroup LayoutGroup { get; @@ -147,11 +160,11 @@ namespace Barotrauma }; TextBox.OnTextChanged += TextChanged; var buttonArea = new GUIFrame(new RectTransform(new Vector2(_relativeButtonAreaWidth, 1.0f), LayoutGroup.RectTransform, Anchor.CenterRight), style: null); - if (!relativeButtonAreaWidth.HasValue) + /*if (!relativeButtonAreaWidth.HasValue) { // Not sure what's the point of this buttonArea.RectTransform.MinSize = new Point(Rect.Height, 0); - } + }*/ PlusButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.5f), buttonArea.RectTransform), "+"); PlusButton.OnButtonDown += () => { diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUIProgressBar.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUIProgressBar.cs index f982c05f3..b8ea1f87d 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUIProgressBar.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUIProgressBar.cs @@ -94,7 +94,7 @@ namespace Barotrauma { spriteBatch.End(); spriteBatch.GraphicsDevice.ScissorRectangle = Rectangle.Intersect(prevScissorRect, sliderRect); - spriteBatch.Begin(SpriteSortMode.Deferred, rasterizerState: GameMain.ScissorTestEnable); + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); } Color currColor = GetCurrentColor(state); diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUIRadioButtonGroup.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUIRadioButtonGroup.cs index f9ee629cb..1ca22acf1 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUIRadioButtonGroup.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUIRadioButtonGroup.cs @@ -9,11 +9,12 @@ namespace Barotrauma { public class GUIRadioButtonGroup : GUIComponent { - private Dictionary radioButtons; //TODO: use children list instead? + private Dictionary radioButtons; //TODO: use children list instead? - public GUIRadioButtonGroup() : base("GUIFrame") + public GUIRadioButtonGroup() : base(null) { - radioButtons = new Dictionary(); + radioButtons = new Dictionary(); + selected = null; } public override bool Enabled @@ -22,28 +23,28 @@ namespace Barotrauma set { base.Enabled = value; - foreach(KeyValuePair rbPair in radioButtons) + foreach(KeyValuePair rbPair in radioButtons) { rbPair.Value.Enabled = value; } } } - public void AddRadioButton(Enum key, GUITickBox radioButton) + public void AddRadioButton(int key, GUITickBox radioButton) { if (selected == key) radioButton.Selected = true; else if (radioButton.Selected) selected = key; radioButton.SetRadioButtonGroup(this); - radioButtons.Add(key, radioButton); + radioButtons.Add((int)key, radioButton); } - public delegate void RadioButtonGroupDelegate(GUIRadioButtonGroup rbg, Enum val); + public delegate void RadioButtonGroupDelegate(GUIRadioButtonGroup rbg, int? val); public RadioButtonGroupDelegate OnSelect = null; public void SelectRadioButton(GUITickBox radioButton) { - foreach (KeyValuePair rbPair in radioButtons) + foreach (KeyValuePair rbPair in radioButtons) { if (radioButton == rbPair.Value) { @@ -53,8 +54,8 @@ namespace Barotrauma } } - private Enum selected; - public Enum Selected + private int? selected; + public int? Selected { get { @@ -63,11 +64,11 @@ namespace Barotrauma set { OnSelect?.Invoke(this, value); - if (selected != null && selected.Equals((Enum)value)) return; + if (selected != null && selected.Equals(value)) return; selected = value; - foreach (KeyValuePair radioButton in radioButtons) + foreach (KeyValuePair radioButton in radioButtons) { - if (radioButton.Key.Equals((Enum)value)) + if (radioButton.Key.Equals(value)) { radioButton.Value.Selected = true; } @@ -80,7 +81,7 @@ namespace Barotrauma { get { - return radioButtons[selected]; + return selected.HasValue ? radioButtons[selected.Value] : null; } } } diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUIScrollBar.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUIScrollBar.cs index 5f6f443c8..42a24b06c 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUIScrollBar.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUIScrollBar.cs @@ -17,6 +17,8 @@ namespace Barotrauma private float barScroll; private float step; + + private Vector2? dragStartPos; public delegate bool OnMovedHandler(GUIScrollBar scrollBar, float barScroll); public OnMovedHandler OnMoved; @@ -162,6 +164,18 @@ namespace Barotrauma } } + public float StepValue + { + get + { + return step * (Range.Y - Range.X); + } + set + { + Step = value / (Range.Y - Range.X); + } + } + public float BarSize { get { return barSize; } @@ -174,6 +188,8 @@ namespace Barotrauma public GUIScrollBar(RectTransform rectT, float barSize = 1, Color? color = null, string style = "", bool? isHorizontal = null) : base(style, rectT) { + CanBeFocused = true; + this.isHorizontal = isHorizontal ?? (Rect.Width > Rect.Height); Frame = new GUIFrame(new RectTransform(Vector2.One, rectT)); GUI.Style.Apply(Frame, IsHorizontal ? "GUIFrameHorizontal" : "GUIFrameVertical", this); @@ -201,11 +217,11 @@ namespace Barotrauma protected override void Update(float deltaTime) { - if (!Visible) return; + if (!Visible) { return; } base.Update(deltaTime); - if (!enabled) return; + if (!enabled) { return; } if (IsBooleanSwitch && (!PlayerInput.LeftButtonHeld() || (GUI.MouseOn != this && !IsParentOf(GUI.MouseOn)))) @@ -221,10 +237,19 @@ namespace Barotrauma if (draggingBar == this) { + if (dragStartPos == null) { dragStartPos = PlayerInput.MousePosition; } + if (!PlayerInput.LeftButtonHeld()) { + if (IsBooleanSwitch && GUI.MouseOn == Bar && Vector2.Distance(dragStartPos.Value, PlayerInput.MousePosition) < 5) + { + BarScroll = BarScroll > 0.5f ? 0.0f : 1.0f; + OnMoved?.Invoke(this, BarScroll); + } OnReleased?.Invoke(this, BarScroll); draggingBar = null; + dragStartPos = null; + } if ((isHorizontal && PlayerInput.MousePosition.X > Rect.X && PlayerInput.MousePosition.X < Rect.Right) || (!isHorizontal && PlayerInput.MousePosition.Y > Rect.Y && PlayerInput.MousePosition.Y < Rect.Bottom)) @@ -237,9 +262,18 @@ namespace Barotrauma if (PlayerInput.LeftButtonClicked()) { draggingBar?.OnReleased?.Invoke(draggingBar, draggingBar.BarScroll); - MoveButton(new Vector2( - Math.Sign(PlayerInput.MousePosition.X - Bar.Rect.Center.X) * Bar.Rect.Width, - Math.Sign(PlayerInput.MousePosition.Y - Bar.Rect.Center.Y) * Bar.Rect.Height)); + if (IsBooleanSwitch) + { + MoveButton(new Vector2( + Math.Sign(PlayerInput.MousePosition.X - Bar.Rect.Center.X) * Rect.Width, + Math.Sign(PlayerInput.MousePosition.Y - Bar.Rect.Center.Y) * Rect.Height)); + } + else + { + MoveButton(new Vector2( + Math.Sign(PlayerInput.MousePosition.X - Bar.Rect.Center.X) * Bar.Rect.Width, + Math.Sign(PlayerInput.MousePosition.Y - Bar.Rect.Center.Y) * Bar.Rect.Height)); + } } } } @@ -270,7 +304,7 @@ namespace Barotrauma BarScroll = newScroll; - if (moveAmount != Vector2.Zero && OnMoved != null) OnMoved(this, BarScroll); + if (moveAmount != Vector2.Zero && OnMoved != null) { OnMoved(this, BarScroll); } } } } diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUITextBlock.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUITextBlock.cs index c54a3db71..18a5cd3e6 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUITextBlock.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUITextBlock.cs @@ -242,10 +242,10 @@ namespace Barotrauma Censor = false; } - public void CalculateHeightFromText() + public void CalculateHeightFromText(int padding = 0) { if (wrappedText == null) { return; } - RectTransform.Resize(new Point(RectTransform.Rect.Width, (int)Font.MeasureString(wrappedText).Y)); + RectTransform.Resize(new Point(RectTransform.Rect.Width, (int)Font.MeasureString(wrappedText).Y + padding)); } public override void ApplyStyle(GUIComponentStyle style) @@ -262,7 +262,7 @@ namespace Barotrauma if (text == null) return; censoredText = ""; - for (int i=0;i 0.0f) GUI.DrawRectangle(spriteBatch, rect, OutlineColor * (currColor.A / 255.0f), false); diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUITextBox.cs index 94e83bef0..6db251444 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUITextBox.cs @@ -9,9 +9,9 @@ using System.Linq; namespace Barotrauma { - delegate void TextBoxEvent(GUITextBox sender, Keys key); + public delegate void TextBoxEvent(GUITextBox sender, Keys key); - class GUITextBox : GUIComponent, IKeyboardSubscriber + public class GUITextBox : GUIComponent, IKeyboardSubscriber { public event TextBoxEvent OnSelected; public event TextBoxEvent OnDeselected; @@ -38,6 +38,7 @@ namespace Barotrauma public bool CaretEnabled { get; set; } public Color? CaretColor { get; set; } + public bool DeselectAfterMessage = true; private int? maxTextLength; @@ -232,6 +233,8 @@ namespace Barotrauma Alignment textAlignment = Alignment.Left, bool wrap = false, string style = "", Color? color = null) : base(style, rectT) { + CanBeFocused = true; + Enabled = true; this.color = color ?? Color.White; frame = new GUIFrame(new RectTransform(Vector2.One, rectT, Anchor.Center), style, color); @@ -477,7 +480,7 @@ namespace Barotrauma } else { - if (PlayerInput.LeftButtonClicked() && selected) Deselect(); + if ((PlayerInput.LeftButtonClicked() || PlayerInput.RightButtonClicked()) && selected) Deselect(); isSelecting = false; state = ComponentState.None; } @@ -656,7 +659,12 @@ namespace Barotrauma switch (command) { case '\b': //backspace - if (selectedCharacters > 0) + if (PlayerInput.KeyDown(Keys.LeftControl) || PlayerInput.KeyDown(Keys.RightControl)) + { + SetText(string.Empty, false); + CaretIndex = Text.Length; + } + else if (selectedCharacters > 0) { RemoveSelectedText(); } @@ -692,6 +700,7 @@ namespace Barotrauma text = memento.Undo(); if (text != Text) { + ClearSelection(); SetText(text, false); CaretIndex = Text.Length; OnTextChanged?.Invoke(this, Text); @@ -701,6 +710,7 @@ namespace Barotrauma text = memento.Redo(); if (text != Text) { + ClearSelection(); SetText(text, false); CaretIndex = Text.Length; OnTextChanged?.Invoke(this, Text); @@ -860,16 +870,12 @@ namespace Barotrauma private void RemoveSelectedText() { if (selectedText.Length == 0) { return; } - if (IsLeftToRight) - { - SetText(Text.Remove(selectionStartIndex, selectedText.Length)); - CaretIndex = Math.Min(Text.Length, selectionStartIndex); - } - else - { - SetText(Text.Remove(selectionEndIndex, selectedText.Length)); - CaretIndex = Math.Min(Text.Length, selectionEndIndex); - } + + selectionStartIndex = Math.Max(0, Math.Min(selectionEndIndex, Math.Min(selectionStartIndex, Text.Length - 1))); + int selectionLength = Math.Min(Text.Length - selectionStartIndex, selectedText.Length); + SetText(Text.Remove(selectionStartIndex, selectionLength)); + CaretIndex = Math.Min(Text.Length, selectionStartIndex); + ClearSelection(); OnTextChanged?.Invoke(this, Text); } diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUITickBox.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUITickBox.cs index dd572c229..5d0c06360 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUITickBox.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUITickBox.cs @@ -1,11 +1,13 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using System; using System.Collections.Generic; namespace Barotrauma { public class GUITickBox : GUIComponent { + private GUILayoutGroup layoutGroup; private GUIFrame box; private GUITextBlock text; @@ -69,14 +71,19 @@ namespace Barotrauma set { text.TextColor = value; } } - public override Rectangle MouseRect + /*public override Rectangle MouseRect { get { if (!CanBeFocused) return Rectangle.Empty; - return ClampMouseRectToParent ? ClampRect(box.Rect) : box.Rect; + Rectangle union = Rectangle.Union(box.Rect, TextBlock.Rect); + Vector2 textPos = TextBlock.Rect.Location.ToVector2() + TextBlock.TextPos + TextBlock.TextOffset; + Vector2 textSize = TextBlock.Font.MeasureString(TextBlock.Text); + union = Rectangle.Union(union, new Rectangle(textPos.ToPoint(), textSize.ToPoint())); + union = Rectangle.Union(union, Rect); + return ClampMouseRectToParent ? ClampRect(union) : union; } - } + }*/ public override ScalableFont Font { @@ -121,7 +128,11 @@ namespace Barotrauma public GUITickBox(RectTransform rectT, string label, ScalableFont font = null, string style = "") : base(null, rectT) { - box = new GUIFrame(new RectTransform(new Point(rectT.Rect.Height, rectT.Rect.Height), rectT, Anchor.CenterLeft) + CanBeFocused = true; + + layoutGroup = new GUILayoutGroup(new RectTransform(Vector2.One, rectT), true); + + box = new GUIFrame(new RectTransform(Vector2.One, layoutGroup.RectTransform, scaleBasis: ScaleBasis.BothHeight) { IsFixedSize = false }, string.Empty, Color.DarkGray) @@ -131,7 +142,11 @@ namespace Barotrauma CanBeFocused = false }; GUI.Style.Apply(box, style == "" ? "GUITickBox" : style); - text = new GUITextBlock(new RectTransform(Vector2.One, rectT, Anchor.CenterLeft) { AbsoluteOffset = new Point(box.Rect.Width, 0) }, label, font: font, textAlignment: Alignment.CenterLeft); + Vector2 textBlockScale = new Vector2((float)(Rect.Width - Rect.Height) / (float)Math.Max(Rect.Width, 1.0), 1.0f); + text = new GUITextBlock(new RectTransform(textBlockScale, layoutGroup.RectTransform), label, font: font, textAlignment: Alignment.CenterLeft) + { + CanBeFocused = false + }; GUI.Style.Apply(text, "GUIButtonHorizontal", this); Enabled = true; @@ -148,9 +163,9 @@ namespace Barotrauma private void ResizeBox() { - box.RectTransform.NonScaledSize = new Point(RectTransform.NonScaledSize.Y); - text.RectTransform.NonScaledSize = new Point(Rect.Width - box.Rect.Width, text.Rect.Height); - text.RectTransform.AbsoluteOffset = new Point(box.Rect.Width, 0); + Vector2 textBlockScale = new Vector2(Math.Max(Rect.Width - box.Rect.Width, 0.0f) / Math.Max(Rect.Width, 1.0f), 1.0f); + text.RectTransform.RelativeSize = textBlockScale; + text.SetTextPos(); } protected override void Update(float deltaTime) diff --git a/Barotrauma/BarotraumaClient/Source/GUI/LoadingScreen.cs b/Barotrauma/BarotraumaClient/Source/GUI/LoadingScreen.cs index 52b571a7c..a9afef032 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/LoadingScreen.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/LoadingScreen.cs @@ -21,11 +21,11 @@ namespace Barotrauma private Video currSplashScreen; private DateTime videoStartTime; - private Queue> pendingSplashScreens = new Queue>(); + private Queue> pendingSplashScreens = new Queue>(); /// - /// Pair.first = filepath, Pair.second = resolution + /// Triplet.first = filepath, Triplet.second = resolution, Triplet.third = audio gain /// - public Queue> PendingSplashScreens + public Queue> PendingSplashScreens { get { @@ -49,7 +49,7 @@ namespace Barotrauma { lock (loadMutex) { - return currSplashScreen != null; + return currSplashScreen != null || pendingSplashScreens.Count > 0; } } } @@ -149,7 +149,7 @@ namespace Barotrauma TitlePosition = new Vector2(GameMain.GraphicsWidth * 0.5f, GameMain.GraphicsHeight * 0.45f); } - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend); + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, samplerState: GUI.SamplerState); graphics.Clear(Color.Black); spriteBatch.Draw(backgroundTexture, BackgroundPosition, null, Color.White * Math.Min(state / 5.0f, 1.0f), 0.0f, @@ -168,7 +168,7 @@ namespace Barotrauma WaterRenderer.Instance.RenderWater(spriteBatch, renderTarget, null); } - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend); + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, samplerState: GUI.SamplerState); titleSprite?.Draw(spriteBatch, TitlePosition, Color.White * Math.Min((state - 1.0f) / 5.0f, 1.0f), scale: titleScale); @@ -280,6 +280,7 @@ namespace Barotrauma try { currSplashScreen = new Video(graphics, GameMain.SoundManager, fileName, (uint)resolution.X, (uint)resolution.Y); + currSplashScreen.AudioGain = newSplashScreen.Third; videoStartTime = DateTime.Now; } catch (Exception e) diff --git a/Barotrauma/BarotraumaClient/Source/GameMain.cs b/Barotrauma/BarotraumaClient/Source/GameMain.cs index 1199a63eb..fff5b55ee 100644 --- a/Barotrauma/BarotraumaClient/Source/GameMain.cs +++ b/Barotrauma/BarotraumaClient/Source/GameMain.cs @@ -166,6 +166,8 @@ namespace Barotrauma get { return loadingScreenOpen; } } + private const GraphicsProfile GfxProfile = GraphicsProfile.Reach; + public GameMain(string[] args) { Content.RootDirectory = "Content"; @@ -173,6 +175,7 @@ namespace Barotrauma GraphicsDeviceManager = new GraphicsDeviceManager(this); GraphicsDeviceManager.IsFullScreen = false; + GraphicsDeviceManager.GraphicsProfile = GfxProfile; GraphicsDeviceManager.ApplyChanges(); Window.Title = "Barotrauma"; @@ -222,7 +225,7 @@ namespace Barotrauma GraphicsHeight = Math.Min(GraphicsDevice.DisplayMode.Height, GraphicsHeight); break; } - GraphicsDeviceManager.GraphicsProfile = GraphicsProfile.Reach; + GraphicsDeviceManager.GraphicsProfile = GfxProfile; GraphicsDeviceManager.PreferredBackBufferFormat = SurfaceFormat.Color; GraphicsDeviceManager.PreferMultiSampling = false; GraphicsDeviceManager.SynchronizeWithVerticalRetrace = Config.VSyncEnabled; @@ -292,6 +295,8 @@ namespace Barotrauma GraphicsWidth = GraphicsDevice.Viewport.Width; GraphicsHeight = GraphicsDevice.Viewport.Height; + ApplyGraphicsSettings(); + ConvertUnits.SetDisplayUnitToSimUnitRatio(Physics.DisplayToSimRation); spriteBatch = new SpriteBatch(GraphicsDevice); @@ -308,8 +313,6 @@ namespace Barotrauma bool canLoadInSeparateThread = true; - ApplyGraphicsSettings(); - loadingCoroutine = CoroutineManager.StartCoroutine(Load(canLoadInSeparateThread), "Load", canLoadInSeparateThread); } @@ -382,9 +385,10 @@ namespace Barotrauma if (Config.EnableSplashScreen) { var pendingSplashScreens = TitleScreen.PendingSplashScreens; - pendingSplashScreens?.Enqueue(new Pair("Content/Splash_UTG.mp4", new Point(1280, 720))); - pendingSplashScreens?.Enqueue(new Pair("Content/Splash_FF.mp4", new Point(1280, 720))); - pendingSplashScreens?.Enqueue(new Pair("Content/Splash_Daedalic.mp4", new Point(1920, 1080))); + float baseVolume = MathHelper.Clamp(Config.SoundVolume * 2.0f, 0.0f, 1.0f); + pendingSplashScreens?.Enqueue(new Triplet("Content/Splash_UTG.mp4", new Point(1280, 720), baseVolume * 0.5f)); + pendingSplashScreens?.Enqueue(new Triplet("Content/Splash_FF.mp4", new Point(1280, 720), baseVolume)); + pendingSplashScreens?.Enqueue(new Triplet("Content/Splash_Daedalic.mp4", new Point(1920, 1080), baseVolume * 0.15f)); } //if not loading in a separate thread, wait for the splash screens to finish before continuing the loading @@ -477,11 +481,6 @@ namespace Barotrauma yield return CoroutineStatus.Running; JobPrefab.LoadAll(GetFilesOfType(ContentType.Jobs)); - // Add any missing jobs from the prefab into Config.JobNamePreferences. - foreach (string job in JobPrefab.List.Keys) - { - if (!Config.JobPreferences.Contains(job)) { Config.JobPreferences.Add(job); } - } NPCConversation.LoadAll(GetFilesOfType(ContentType.NPCConversations)); @@ -779,7 +778,17 @@ namespace Barotrauma } } - GUI.ClearUpdateList(); +#if DEBUG + if (GameMain.NetworkMember == null) + { + if (PlayerInput.KeyHit(Keys.P) && !(GUI.KeyboardDispatcher.Subscriber is GUITextBox)) + { + DebugConsole.Paused = !DebugConsole.Paused; + } + } +#endif + + GUI.ClearUpdateList(); paused = (DebugConsole.IsOpen || GUI.PauseMenuOpen || GUI.SettingsMenuOpen || Tutorial.ContentRunning || DebugConsole.Paused) && (NetworkMember == null || !NetworkMember.GameStarted); @@ -800,7 +809,7 @@ namespace Barotrauma DebugConsole.AddToGUIUpdateList(); - DebugConsole.Update(this, (float)Timing.Step); + DebugConsole.Update((float)Timing.Step); paused = paused || (DebugConsole.IsOpen && (NetworkMember == null || !NetworkMember.GameStarted)); if (!paused) @@ -878,6 +887,7 @@ namespace Barotrauma { spriteBatch.Begin(); GUI.DrawRectangle(spriteBatch, GUI.MouseOn.MouseRect, Color.Lime); + GUI.DrawRectangle(spriteBatch, GUI.MouseOn.Rect, Color.Cyan); spriteBatch.End(); } @@ -887,6 +897,61 @@ namespace Barotrauma PerformanceCounter.DrawTimeGraph.Update(sw.ElapsedTicks / (float)TimeSpan.TicksPerMillisecond); } + + public static void QuitToMainMenu(bool save, bool showVerificationPrompt) + { + if (showVerificationPrompt) + { + string text = (Screen.Selected is CharacterEditor.CharacterEditorScreen || Screen.Selected is SubEditorScreen) ? "PauseMenuQuitVerificationEditor" : "PauseMenuQuitVerification"; + var msgBox = new GUIMessageBox("", TextManager.Get(text), new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }) + { + UserData = "verificationprompt" + }; + msgBox.Buttons[0].OnClicked = (yesBtn, userdata) => + { + QuitToMainMenu(save); + return true; + }; + msgBox.Buttons[0].OnClicked += msgBox.Close; + msgBox.Buttons[1].OnClicked += msgBox.Close; + } + + } + + public static void QuitToMainMenu(bool save) + { + if (save) + { + SaveUtil.SaveGame(GameMain.GameSession.SavePath); + } + + if (GameMain.Client != null) + { + GameMain.Client.Disconnect(); + GameMain.Client = null; + } + + CoroutineManager.StopCoroutines("EndCinematic"); + + if (GameMain.GameSession != null) + { + if (Tutorial.Initialized) + { + ((TutorialMode)GameMain.GameSession.GameMode).Tutorial?.Stop(); + } + + if (GameSettings.SendUserStatistics) + { + Mission mission = GameMain.GameSession.Mission; + GameAnalyticsManager.AddDesignEvent("QuitRound:" + (save ? "Save" : "NoSave")); + GameAnalyticsManager.AddDesignEvent("EndRound:" + (mission == null ? "NoMission" : (mission.Completed ? "MissionCompleted" : "MissionFailed"))); + } + GameMain.GameSession = null; + } + GUIMessageBox.CloseAll(); + GameMain.MainMenuScreen.Select(); + } + public void ShowCampaignDisclaimer(Action onContinue = null) { var msgBox = new GUIMessageBox(TextManager.Get("CampaignDisclaimerTitle"), TextManager.Get("CampaignDisclaimerText"), @@ -986,8 +1051,19 @@ namespace Barotrauma { if (NetworkMember != null) NetworkMember.Disconnect(); SteamManager.ShutDown(); - if (GameSettings.SendUserStatistics) GameAnalytics.OnQuit(); - if (GameSettings.SaveDebugConsoleLogs) DebugConsole.SaveLogs(); + + try + { + SaveUtil.CleanUnnecessarySaveFiles(); + } + catch (Exception e) + { + DebugConsole.ThrowError("Error while cleaning unnecessary save files", e); + } + + if (GameSettings.SendUserStatistics){ GameAnalytics.OnQuit(); } + if (GameSettings.SaveDebugConsoleLogs) { DebugConsole.SaveLogs(); } + base.OnExiting(sender, args); } diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/Source/GameSession/CrewManager.cs index a853eaeda..dd7f32d52 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/CrewManager.cs @@ -116,7 +116,8 @@ namespace Barotrauma //Spacing = (int)(3 * GUI.Scale), ScrollBarEnabled = false, ScrollBarVisible = false, - CanBeFocused = false + CanBeFocused = true, + OnSelected = (component, userdata) => false }; scrollButtonUp = new GUIButton(new RectTransform(scrollButtonSize, crewArea.RectTransform, Anchor.TopLeft, Pivot.TopLeft), "", Alignment.Center, "GUIButtonVerticalArrow") @@ -443,6 +444,13 @@ namespace Barotrauma ToolTip = characterToolTip }; + + if (GameMain.GameSession?.GameMode?.Mission is CombatMission combatMission) + { + new GUIFrame(new RectTransform(Vector2.One, characterArea.RectTransform), style: "InnerGlow", + color: character.TeamID == Character.TeamType.Team1 ? Color.SteelBlue : Color.OrangeRed); + } + var characterName = new GUITextBlock(new RectTransform(new Point(characterArea.Rect.Width - characterImage.Rect.Width - soundIcon.Rect.Width - 10, characterArea.Rect.Height), characterArea.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(soundIcon.Rect.Width + 10, 0) }, character.Name, textColor: frame.Color, font: GUI.SmallFont, wrap: true) @@ -1016,7 +1024,7 @@ namespace Barotrauma ToggleCrewAreaOpen = true; var characterElement = characterListBox.Content.FindChild(character); GUIButton orderBtn = characterElement.FindChild(order, recursive: true) as GUIButton; - if (orderBtn.Frame.FlashTimer <= 0) + if (orderBtn.FlashTimer <= 0) { orderBtn.Flash(color, 1.5f, false, flashRectInflate); } @@ -1360,7 +1368,7 @@ namespace Barotrauma public void UpdateReports(float deltaTime) { bool canIssueOrders = false; - if (Character.Controlled?.CurrentHull != null && Character.Controlled.SpeechImpediment < 100.0f) + if (Character.Controlled?.CurrentHull?.Submarine != null && Character.Controlled.SpeechImpediment < 100.0f) { WifiComponent radio = GetHeadset(Character.Controlled, true); canIssueOrders = radio != null && radio.CanTransmit(); diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/MultiPlayerCampaign.cs index 26b195c90..73c7dd7a0 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/MultiPlayerCampaign.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Barotrauma.Extensions; namespace Barotrauma { @@ -13,25 +14,24 @@ namespace Barotrauma private UInt16 startWatchmanID, endWatchmanID; - public static GUIComponent StartCampaignSetup( IEnumerable submarines, IEnumerable saveFiles) + public static void StartCampaignSetup(IEnumerable saveFiles) { - GUIFrame background = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), style: "GUIBackgroundBlocker"); + var parent = GameMain.NetLobbyScreen.CampaignSetupFrame; + parent.ClearChildren(); + parent.Visible = true; + GameMain.NetLobbyScreen.HighlightMode(2); - GUIFrame setupBox = new GUIFrame(new RectTransform(new Vector2(0.25f, 0.45f), background.RectTransform, Anchor.Center) { MinSize = new Point(500, 550) }); - var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), setupBox.RectTransform, Anchor.Center)) + var layout = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform, Anchor.Center)) { Stretch = true }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), paddedFrame.RectTransform,Anchor.TopCenter), - TextManager.Get("CampaignSetup"), font: GUI.LargeFont); - - var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.07f), paddedFrame.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.1f) }, isHorizontal: true) + var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.07f), layout.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.1f) }, isHorizontal: true) { RelativeSpacing = 0.02f }; - var campaignContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), paddedFrame.RectTransform, Anchor.BottomLeft), style: "InnerFrame") + var campaignContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), layout.RectTransform, Anchor.BottomLeft), style: "InnerFrame") { CanBeFocused = false }; @@ -39,9 +39,9 @@ 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, submarines, saveFiles); + var campaignSetupUI = new CampaignSetupUI(true, newCampaignContainer, loadCampaignContainer, null, saveFiles); - var newCampaignButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonContainer.RectTransform), + var newCampaignButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonContainer.RectTransform), TextManager.Get("NewCampaign"), style: "GUITabButton") { OnClicked = (btn, obj) => @@ -52,7 +52,7 @@ namespace Barotrauma } }; - var loadCampaignButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.00f), buttonContainer.RectTransform), + var loadCampaignButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.00f), buttonContainer.RectTransform), TextManager.Get("LoadCampaign"), style: "GUITabButton") { OnClicked = (btn, obj) => @@ -67,20 +67,6 @@ namespace Barotrauma campaignSetupUI.StartNewGame = GameMain.Client.SetupNewCampaign; campaignSetupUI.LoadGame = GameMain.Client.SetupLoadCampaign; - - var cancelButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.1f), paddedFrame.RectTransform, Anchor.BottomLeft), - TextManager.Get("Cancel"), style: "GUIButtonLarge") - { - IgnoreLayoutGroups = true, - OnClicked = (btn, obj) => - { - background.Visible = false; - - return true; - } - }; - - return background; } public override void Update(float deltaTime) diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/CaptainTutorial.cs b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/CaptainTutorial.cs index 73e18c6f2..a9b4039af 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/CaptainTutorial.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/CaptainTutorial.cs @@ -255,9 +255,9 @@ namespace Barotrauma.Tutorials } if (order.Options[orderIndex] == option) { - if (GameMain.GameSession.CrewManager.OrderOptionButtons[i].Frame.FlashTimer <= 0) + if (GameMain.GameSession.CrewManager.OrderOptionButtons[i].FlashTimer <= 0) { - GameMain.GameSession.CrewManager.OrderOptionButtons[i].Frame.Flash(highlightColor); + GameMain.GameSession.CrewManager.OrderOptionButtons[i].Flash(highlightColor); } } diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/EngineerTutorial.cs b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/EngineerTutorial.cs index 87188c524..a458bb670 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/EngineerTutorial.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/EngineerTutorial.cs @@ -370,9 +370,9 @@ namespace Barotrauma.Tutorials } else if (IsSelectedItem(engineer_brokenJunctionBox) && repairableJunctionBoxComponent.CurrentFixer == null) { - if (repairableJunctionBoxComponent.RepairButton.Frame.FlashTimer <= 0) + if (repairableJunctionBoxComponent.RepairButton.FlashTimer <= 0) { - repairableJunctionBoxComponent.RepairButton.Frame.Flash(); + repairableJunctionBoxComponent.RepairButton.Flash(); } } yield return null; diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/MechanicTutorial.cs b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/MechanicTutorial.cs index db5d5f16f..4f58acb60 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/MechanicTutorial.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/MechanicTutorial.cs @@ -416,9 +416,9 @@ namespace Barotrauma.Tutorials if (mechanic_deconstructor.InputContainer.Inventory.FindItemByIdentifier("oxygentank") != null && !mechanic_deconstructor.IsActive) { - if (mechanic_deconstructor.ActivateButton.Frame.FlashTimer <= 0) + if (mechanic_deconstructor.ActivateButton.FlashTimer <= 0) { - mechanic_deconstructor.ActivateButton.Frame.Flash(highlightColor, 1.5f, false); + mechanic_deconstructor.ActivateButton.Flash(highlightColor, 1.5f, false); } } } @@ -452,9 +452,9 @@ namespace Barotrauma.Tutorials } else if (mechanic_fabricator.InputContainer.Inventory.FindItemByIdentifier("aluminium") != null && mechanic_fabricator.InputContainer.Inventory.FindItemByIdentifier("sodium") != null && !mechanic_fabricator.IsActive) { - if (mechanic_fabricator.ActivateButton.Frame.FlashTimer <= 0) + if (mechanic_fabricator.ActivateButton.FlashTimer <= 0) { - mechanic_fabricator.ActivateButton.Frame.Flash(highlightColor, 1.5f, false); + mechanic_fabricator.ActivateButton.Flash(highlightColor, 1.5f, false); } } else if (mechanic.Inventory.FindItemByIdentifier("aluminium") != null || mechanic.Inventory.FindItemByIdentifier("sodium") != null) @@ -544,9 +544,9 @@ namespace Barotrauma.Tutorials } else if (IsSelectedItem(mechanic_brokenPump.Item) && repairablePumpComponent.CurrentFixer == null) { - if (repairablePumpComponent.RepairButton.Frame.FlashTimer <= 0) + if (repairablePumpComponent.RepairButton.FlashTimer <= 0) { - repairablePumpComponent.RepairButton.Frame.Flash(); + repairablePumpComponent.RepairButton.Flash(); } } } diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/GameSession.cs b/Barotrauma/BarotraumaClient/Source/GameSession/GameSession.cs index 81b5040f3..6816e6f26 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/GameSession.cs @@ -18,6 +18,11 @@ namespace Barotrauma private bool ToggleInfoFrame() { + if (GameMain.NetworkMember != null && GameMain.NetLobbyScreen != null) + { + if (GameMain.NetLobbyScreen.HeadSelectionList != null) { GameMain.NetLobbyScreen.HeadSelectionList.Visible = false; } + if (GameMain.NetLobbyScreen.JobSelectionFrame != null) { GameMain.NetLobbyScreen.JobSelectionFrame.Visible = false; } + } if (infoFrame == null) { CreateInfoFrame(); @@ -37,7 +42,7 @@ namespace Barotrauma infoFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas), style: "GUIBackgroundBlocker"); - var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.35f), infoFrame.RectTransform, Anchor.Center) { MinSize = new Point(width, height) }); + var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.35f), infoFrame.RectTransform, Anchor.Center) { MinSize = new Point(width, height), RelativeOffset = new Vector2(0.0f, 0.033f) }); var paddedFrame = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.9f), innerFrame.RectTransform, Anchor.Center), style: null); var buttonArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.08f), paddedFrame.RectTransform), isHorizontal: true) @@ -144,6 +149,12 @@ namespace Barotrauma if (GUI.DisableHUD) return; GameMode?.AddToGUIUpdateList(); infoFrame?.AddToGUIUpdateList(); + + if (GameMain.NetworkMember != null) + { + GameMain.NetLobbyScreen?.HeadSelectionList?.AddToGUIUpdateList(); + GameMain.NetLobbyScreen?.JobSelectionFrame?.AddToGUIUpdateList(); + } } partial void UpdateProjSpecific(float deltaTime) @@ -163,7 +174,24 @@ namespace Barotrauma ToggleInfoFrame(); } - infoFrame?.UpdateManually(deltaTime); + if (GameMain.NetworkMember != null) + { + if (GameMain.NetLobbyScreen?.HeadSelectionList != null) + { + if (PlayerInput.LeftButtonDown() && !GUI.IsMouseOn(GameMain.NetLobbyScreen.HeadSelectionList)) + { + if (GameMain.NetLobbyScreen.HeadSelectionList != null) { GameMain.NetLobbyScreen.HeadSelectionList.Visible = false; } + } + } + if (GameMain.NetLobbyScreen?.JobSelectionFrame != null) + { + if (PlayerInput.LeftButtonDown() && !GUI.IsMouseOn(GameMain.NetLobbyScreen.JobSelectionFrame)) + { + GameMain.NetLobbyScreen.JobList.Deselect(); + if (GameMain.NetLobbyScreen.JobSelectionFrame != null) { GameMain.NetLobbyScreen.JobSelectionFrame.Visible = false; } + } + } + } } public void Draw(SpriteBatch spriteBatch) @@ -171,7 +199,7 @@ namespace Barotrauma if (GUI.DisableHUD) return; GameMode?.Draw(spriteBatch); - infoFrame?.DrawManually(spriteBatch); + //infoFrame?.DrawManually(spriteBatch); } } } diff --git a/Barotrauma/BarotraumaClient/Source/GameSettings.cs b/Barotrauma/BarotraumaClient/Source/GameSettings.cs index b4b7e2a49..064f183c0 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSettings.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSettings.cs @@ -152,8 +152,7 @@ namespace Barotrauma var languageDD = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.045f), generalLayoutGroup.RectTransform)); foreach (string language in TextManager.AvailableLanguages) { - //TODO: display the name of the language in the target language? - languageDD.AddItem(language, language); + languageDD.AddItem(TextManager.GetTranslatedLanguageName(language), language); } languageDD.SelectItem(TextManager.Language); languageDD.OnSelected = (guiComponent, obj) => @@ -356,7 +355,7 @@ namespace Barotrauma }; lightScrollBar.OnMoved(lightScrollBar, lightScrollBar.BarScroll); - new GUITickBox(new RectTransform(tickBoxScale, rightColumn.RectTransform, scaleBasis: ScaleBasis.BothHeight), TextManager.Get("SpecularLighting")) + /*new GUITickBox(new RectTransform(tickBoxScale, rightColumn.RectTransform, scaleBasis: ScaleBasis.BothHeight), TextManager.Get("SpecularLighting")) { ToolTip = TextManager.Get("SpecularLightingToolTip"), Selected = SpecularityEnabled, @@ -366,7 +365,7 @@ namespace Barotrauma UnsavedSettings = true; return true; } - }; + };*/ new GUITickBox(new RectTransform(tickBoxScale, rightColumn.RectTransform, scaleBasis: ScaleBasis.BothHeight), TextManager.Get("ChromaticAberration")) { @@ -508,8 +507,8 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), voipSettings.RectTransform), TextManager.Get("VoiceChat")); - IList deviceNames = Alc.GetStringList((IntPtr)null, Alc.CaptureDeviceSpecifier); - foreach (string name in deviceNames) + CaptureDeviceNames = Alc.GetStringList((IntPtr)null, Alc.CaptureDeviceSpecifier); + foreach (string name in CaptureDeviceNames) { DebugConsole.NewMessage(name + " " + name.Length.ToString(), Color.Lime); } @@ -524,19 +523,19 @@ namespace Barotrauma return true; }; - if (string.IsNullOrWhiteSpace(VoiceCaptureDevice) || !(deviceNames?.Contains(VoiceCaptureDevice) ?? false)) + if (string.IsNullOrWhiteSpace(VoiceCaptureDevice) || !(CaptureDeviceNames?.Contains(VoiceCaptureDevice) ?? false)) { - VoiceCaptureDevice = deviceNames?.Count > 0 ? deviceNames[0] : null; + VoiceCaptureDevice = CaptureDeviceNames?.Count > 0 ? CaptureDeviceNames[0] : null; } if (string.IsNullOrWhiteSpace(VoiceCaptureDevice)) { VoiceSetting = VoiceMode.Disabled; } #if (!OSX) - var deviceList = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.15f), voipSettings.RectTransform), TrimAudioDeviceName(VoiceCaptureDevice), deviceNames.Count); - if (deviceNames?.Count > 0) + var deviceList = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.15f), voipSettings.RectTransform), TrimAudioDeviceName(VoiceCaptureDevice), CaptureDeviceNames.Count); + if (CaptureDeviceNames?.Count > 0) { - foreach (string name in deviceNames) + foreach (string name in CaptureDeviceNames) { deviceList.AddItem(TrimAudioDeviceName(name), name); } @@ -571,12 +570,12 @@ namespace Barotrauma ToolTip = TextManager.Get("RefreshDefaultDeviceToolTip"), OnClicked = (bt, userdata) => { - deviceNames = Alc.GetStringList((IntPtr)null, Alc.CaptureDeviceSpecifier); - if (deviceNames?.Count > 0) + CaptureDeviceNames = Alc.GetStringList((IntPtr)null, Alc.CaptureDeviceSpecifier); + if (CaptureDeviceNames?.Count > 0) { - if (VoiceCaptureDevice == deviceNames[0]) return true; + if (VoiceCaptureDevice == CaptureDeviceNames[0]) return true; - VoipCapture.ChangeCaptureDevice(deviceNames[0]); + VoipCapture.ChangeCaptureDevice(CaptureDeviceNames[0]); currentDeviceTextBlock.Text = TextManager.AddPunctuation(':', TextManager.Get("CurrentDevice"), TrimAudioDeviceName(VoiceCaptureDevice)); currentDeviceTextBlock.Flash(Color.Blue); } @@ -598,12 +597,12 @@ namespace Barotrauma for (int i = 0; i < 3; i++) { string langStr = "VoiceMode." + ((VoiceMode)i).ToString(); - var tick = new GUITickBox(new RectTransform(tickBoxScale / 0.4f, voipSettings.RectTransform, scaleBasis: ScaleBasis.BothHeight), TextManager.Get(langStr)) + var tick = new GUITickBox(new RectTransform(tickBoxScale / 0.4f, voipSettings.RectTransform, scaleBasis: ScaleBasis.BothHeight), TextManager.Get(langStr), style: "GUIRadioButton") { ToolTip = TextManager.Get(langStr + "ToolTip") }; - voiceMode.AddRadioButton((VoiceMode)i, tick); + voiceMode.AddRadioButton(i, tick); } var micVolumeText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), voipSettings.RectTransform), TextManager.Get("MicrophoneVolume")); @@ -611,10 +610,10 @@ namespace Barotrauma barSize: 0.05f) { UserData = micVolumeText, - BarScroll = (float)Math.Sqrt(MathUtils.InverseLerp(0.2f, 5.0f, MicrophoneVolume)), + BarScroll = (float)Math.Sqrt(MathUtils.InverseLerp(0.2f, MaxMicrophoneVolume, MicrophoneVolume)), OnMoved = (scrollBar, scroll) => { - MicrophoneVolume = MathHelper.Lerp(0.2f, 10.0f, scroll * scroll); + MicrophoneVolume = MathHelper.Lerp(0.2f, MaxMicrophoneVolume, scroll * scroll); MicrophoneVolume = (float)Math.Round(MicrophoneVolume, 1); ChangeSliderText(scrollBar, MicrophoneVolume); scrollBar.Step = 0.05f; @@ -667,7 +666,7 @@ namespace Barotrauma return true; }; - voiceMode.OnSelect = (GUIRadioButtonGroup rbg, Enum value) => + voiceMode.OnSelect = (GUIRadioButtonGroup rbg, int? value) => { if (rbg.Selected != null && rbg.Selected.Equals(value)) return; try @@ -708,7 +707,7 @@ namespace Barotrauma VoiceSetting = VoiceMode.Disabled; } }; - voiceMode.Selected = VoiceSetting; + voiceMode.Selected = (int)VoiceSetting; if (string.IsNullOrWhiteSpace(VoiceCaptureDevice)) { voiceMode.Enabled = false; @@ -1157,7 +1156,7 @@ namespace Barotrauma { ApplySettings(); if (Screen.Selected != GameMain.MainMenuScreen) GUI.SettingsMenuOpen = false; - if (contentPackageSelectionDirty) + if (contentPackageSelectionDirty || ContentPackage.List.Any(cp => cp.NeedsRestart)) { new GUIMessageBox(TextManager.Get("RestartRequiredLabel"), TextManager.Get("RestartRequiredGeneric")); } diff --git a/Barotrauma/BarotraumaClient/Source/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/Source/Items/CharacterInventory.cs index 4f2a5eb2d..c8f6b7e9d 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/CharacterInventory.cs @@ -718,11 +718,11 @@ namespace Barotrauma if (item.ParentInventory != this) { - //in another inventory -> attempt to place in the character's inventory - if (item.ParentInventory.Locked || item.ParentInventory == null) + if (item.ParentInventory == null || item.ParentInventory.Locked) { return QuickUseAction.None; } + //in another inventory -> attempt to place in the character's inventory else if (allowInventorySwap) { if (item.Container == null || character.Inventory.FindIndex(item.Container) == -1) // Not a subinventory in the character's inventory diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/ItemContainer.cs index 5fafa9498..8f1972c6d 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/ItemContainer.cs @@ -64,6 +64,10 @@ namespace Barotrauma.Items.Components [Serialize(null, false, description: "An optional text displayed above the item's inventory.")] public string UILabel { get; set; } + [Serialize(true, false, description: "Should an indicator displaying the state of the contained items be displayed on this item's inventory slot. "+ + "If this item can only contain one item, the indicator will display the condition of the contained item, otherwise it will indicate how full the item is.")] + public bool ShowContainedStateIndicator { get; set; } + [Serialize(false, false, description: "If enabled, the condition of this item is displayed in the indicator that would normally show the state of the contained items." + " May be useful for items such as ammo boxes and magazines that spawn projectiles as needed," + " and use the condition to determine how many projectiles can be spawned in total.")] diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Controller.cs index e4547a6c2..f6ffb5e1d 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Controller.cs @@ -38,7 +38,7 @@ namespace Barotrauma.Items.Components private void ToggleCrewArea(bool value, bool storeOriginalState) { - var crewManager = GameMain.GameSession.CrewManager; + var crewManager = GameMain.GameSession?.CrewManager; if (crewManager == null) { return; } if (storeOriginalState) @@ -50,7 +50,7 @@ namespace Barotrauma.Items.Components private void ToggleChatBox(bool value, bool storeOriginalState) { - var crewManager = GameMain.GameSession.CrewManager; + var crewManager = GameMain.GameSession?.CrewManager; if (crewManager == null) { return; } if (crewManager.IsSinglePlayer) diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/MiniMap.cs index b9f7fc697..1ccba72ca 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/MiniMap.cs @@ -114,7 +114,7 @@ namespace Barotrauma.Items.Components private void DrawHUDFront(SpriteBatch spriteBatch, GUICustomComponent container) { - if (voltage < minVoltage) + if (Voltage < MinVoltage) { Vector2 textSize = GUI.Font.MeasureString(noPowerTip); Vector2 textPos = GuiFrame.Rect.Center.ToVector2(); @@ -164,7 +164,7 @@ namespace Barotrauma.Items.Components } } - if (voltage < minVoltage) + if (Voltage < MinVoltage) { return; } diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Pump.cs index 59be09f8d..6b7797bb0 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Pump.cs @@ -128,6 +128,7 @@ namespace Barotrauma.Items.Components public override void OnItemLoaded() { + base.OnItemLoaded(); if (pumpSpeedSlider != null) { pumpSpeedSlider.BarScroll = (flowPercentage + 100.0f) / 200.0f; diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Reactor.cs index b64362833..5d725464e 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Reactor.cs @@ -290,6 +290,7 @@ namespace Barotrauma.Items.Components public override void OnItemLoaded() { + base.OnItemLoaded(); turbineOutputScrollBar.BarScroll = targetTurbineOutput / 100.0f; fissionRateScrollBar.BarScroll = targetFissionRate / 100.0f; var itemContainer = item.GetComponent(); @@ -604,14 +605,15 @@ namespace Barotrauma.Items.Components protected override void RemoveComponentSpecific() { - graphLine.Remove(); - fissionRateMeter.Remove(); - turbineOutputMeter.Remove(); - meterPointer.Remove(); - sectorSprite.Remove(); - tempMeterFrame.Remove(); - tempMeterBar.Remove(); - tempRangeIndicator.Remove(); + base.RemoveComponentSpecific(); + graphLine?.Remove(); + fissionRateMeter?.Remove(); + turbineOutputMeter?.Remove(); + meterPointer?.Remove(); + sectorSprite?.Remove(); + tempMeterFrame?.Remove(); + tempMeterBar?.Remove(); + tempRangeIndicator?.Remove(); } public void ClientWrite(IWriteMessage msg, object[] extraData = null) diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Sonar.cs index 330ef05df..e85555c7b 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Sonar.cs @@ -54,7 +54,7 @@ namespace Barotrauma.Items.Components //float = strength of the disruption, between 0-1 List> disruptedDirections = new List>(); - private static Color[] blipColorGradient = + private static readonly Color[] blipColorGradient = { Color.TransparentBlack, new Color(0, 50, 160), @@ -162,9 +162,9 @@ namespace Barotrauma.Items.Components signalWarningText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), paddedControlContainer.RectTransform), "", Color.Orange, textAlignment: Alignment.Center); GUIRadioButtonGroup sonarMode = new GUIRadioButtonGroup(); - sonarMode.AddRadioButton(Mode.Active, activeTickBox); - sonarMode.AddRadioButton(Mode.Passive, passiveTickBox); - sonarMode.Selected = Mode.Passive; + sonarMode.AddRadioButton((int)Mode.Active, activeTickBox); + sonarMode.AddRadioButton((int)Mode.Passive, passiveTickBox); + sonarMode.Selected = (int)Mode.Passive; GuiFrame.CanBeFocused = false; @@ -226,6 +226,7 @@ namespace Barotrauma.Items.Components public override void OnItemLoaded() { + base.OnItemLoaded(); zoomSlider.BarScroll = MathUtils.InverseLerp(MinZoom, MaxZoom, zoom); //make the sonarView customcomponent render the steering view so it gets drawn in front of the sonar item.GetComponent()?.AttachToSonarHUD(sonarView); @@ -434,7 +435,7 @@ namespace Barotrauma.Items.Components if (distSqr > t.SoundRange * t.SoundRange * 2) { continue; } float dist = (float)Math.Sqrt(distSqr); - if (dist > prevPassivePingRadius * Range && dist <= passivePingRadius * Range) + if (dist > prevPassivePingRadius * Range && dist <= passivePingRadius * Range && Rand.Int(sonarBlips.Count) < 500) { Ping(t.WorldPosition, transducerCenter, Math.Min(t.SoundRange, range * 0.5f) * displayScale, 0, displayScale, Math.Min(t.SoundRange, range * 0.5f), @@ -675,14 +676,14 @@ namespace Barotrauma.Items.Components } else if (startOutside) { - if (MathUtils.GetLineCircleIntersections(Vector2.Zero, DisplayRadius, end, start, true, out Vector2? intersection1, out Vector2? intersection2) == 1) + if (MathUtils.GetLineCircleIntersections(Vector2.Zero, DisplayRadius, end, start, true, out Vector2? intersection1, out _) == 1) { DrawLineSprite(spriteBatch, center + intersection1.Value, center + end, color, width: width); } } else if (endOutside) { - if (MathUtils.GetLineCircleIntersections(Vector2.Zero, DisplayRadius, start, end, true, out Vector2? intersection1, out Vector2? intersection2) == 1) + if (MathUtils.GetLineCircleIntersections(Vector2.Zero, DisplayRadius, start, end, true, out Vector2? intersection1, out _) == 1) { DrawLineSprite(spriteBatch, center + start, center + intersection1.Value, color, width: width); } @@ -750,7 +751,7 @@ namespace Barotrauma.Items.Components { size.Y = 0.0f; } - GUI.DrawLine(spriteBatch, center + offset - size, center + offset + size, Color.LightGreen, width: (int)(zoom * 2.5f)); + GUI.DrawLine(spriteBatch, center + offset - size, center + offset + size, Color.LightGreen * signalStrength, width: (int)(zoom * 2.5f)); } } @@ -769,8 +770,6 @@ namespace Barotrauma.Items.Components Vector2 targetPortDiff = (steering.DockingTarget.Item.WorldPosition - transducerCenter) * scale; Vector2 targetPortPos = new Vector2(targetPortDiff.X, -targetPortDiff.Y); - Vector2 midPos = (sourcePortPos + targetPortPos) / 2.0f; - System.Diagnostics.Debug.Assert(steering.ActiveDockingSource.IsHorizontal == steering.DockingTarget.IsHorizontal); Vector2 diff = steering.DockingTarget.Item.WorldPosition - steering.ActiveDockingSource.Item.WorldPosition; float dist = diff.Length(); @@ -851,7 +850,6 @@ namespace Barotrauma.Items.Components private void UpdateDisruptions(Vector2 pingSource, float worldPingRadius, float worldPrevPingRadius) { float worldPingRadiusSqr = worldPingRadius * worldPingRadius; - float worldPrevPingRadiusSqr = worldPrevPingRadius * worldPrevPingRadius; disruptedDirections.Clear(); if (Level.Loaded == null) { return; } diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Steering.cs index 595592103..f0dc6be87 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Steering.cs @@ -159,9 +159,9 @@ namespace Barotrauma.Items.Components }; GUIRadioButtonGroup modes = new GUIRadioButtonGroup(); - modes.AddRadioButton(Mode.AutoPilot, autopilotTickBox); - modes.AddRadioButton(Mode.Manual, manualTickBox); - modes.Selected = Mode.Manual; + modes.AddRadioButton((int)Mode.AutoPilot, autopilotTickBox); + modes.AddRadioButton((int)Mode.Manual, manualTickBox); + modes.Selected = (int)Mode.Manual; var autoPilotControls = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.6f), paddedControlContainer.RectTransform), "InnerFrame"); var paddedAutoPilotControls = new GUILayoutGroup(new RectTransform(new Vector2(0.8f), autoPilotControls.RectTransform, Anchor.Center)) @@ -171,7 +171,7 @@ namespace Barotrauma.Items.Components }; maintainPosTickBox = new GUITickBox(new RectTransform(new Vector2(0.2f, 0.2f), paddedAutoPilotControls.RectTransform), - TextManager.Get("SteeringMaintainPos"), font: GUI.SmallFont) + TextManager.Get("SteeringMaintainPos"), font: GUI.SmallFont, style: "GUIRadioButton") { Enabled = false, Selected = maintainPos, @@ -208,7 +208,7 @@ namespace Barotrauma.Items.Components levelStartTickBox = new GUITickBox(new RectTransform(new Vector2(0.2f, 0.2f), paddedAutoPilotControls.RectTransform), GameMain.GameSession?.StartLocation == null ? "" : ToolBox.LimitString(GameMain.GameSession.StartLocation.Name, 20), - font: GUI.SmallFont) + font: GUI.SmallFont, style: "GUIRadioButton") { Enabled = false, Selected = levelStartSelected, @@ -235,7 +235,7 @@ namespace Barotrauma.Items.Components levelEndTickBox = new GUITickBox(new RectTransform(new Vector2(0.2f, 0.2f), paddedAutoPilotControls.RectTransform), GameMain.GameSession?.EndLocation == null ? "" : ToolBox.LimitString(GameMain.GameSession.EndLocation.Name, 20), - font: GUI.SmallFont) + font: GUI.SmallFont, style: "GUIRadioButton") { Enabled = false, Selected = levelEndSelected, @@ -263,11 +263,11 @@ namespace Barotrauma.Items.Components autoPilotControlsDisabler = new GUIFrame(new RectTransform(Vector2.One, autoPilotControls.RectTransform), "InnerFrame"); GUIRadioButtonGroup destinations = new GUIRadioButtonGroup(); - destinations.AddRadioButton(Destination.MaintainPos, maintainPosTickBox); - destinations.AddRadioButton(Destination.LevelStart, levelStartTickBox); - destinations.AddRadioButton(Destination.LevelEnd, levelEndTickBox); - destinations.Selected = maintainPos ? Destination.MaintainPos : - levelStartSelected ? Destination.LevelStart : Destination.LevelEnd; + destinations.AddRadioButton((int)Destination.MaintainPos, maintainPosTickBox); + destinations.AddRadioButton((int)Destination.LevelStart, levelStartTickBox); + destinations.AddRadioButton((int)Destination.LevelEnd, levelEndTickBox); + destinations.Selected = (int)(maintainPos ? Destination.MaintainPos : + levelStartSelected ? Destination.LevelStart : Destination.LevelEnd); string steeringVelX = TextManager.Get("SteeringVelocityX"); string steeringVelY = TextManager.Get("SteeringVelocityY"); @@ -442,7 +442,7 @@ namespace Barotrauma.Items.Components int x = rect.X; int y = rect.Y; - if (voltage < minVoltage && currPowerConsumption > 0.0f) return; + if (Voltage < MinVoltage) { return; } Rectangle velRect = new Rectangle(x + 20, y + 20, width - 40, height - 40); Vector2 displaySubPos = (-sonar.DisplayOffset * sonar.Zoom) / sonar.Range * sonar.DisplayRadius * sonar.Zoom; @@ -649,7 +649,7 @@ namespace Barotrauma.Items.Components autoPilotControlsDisabler.Visible = !AutoPilot; - if (voltage < minVoltage && currPowerConsumption > 0.0f) + if (Voltage < MinVoltage) { tipContainer.Visible = true; tipContainer.Text = noPowerTip; @@ -819,6 +819,7 @@ namespace Barotrauma.Items.Components protected override void RemoveComponentSpecific() { + base.RemoveComponentSpecific(); maintainPosIndicator?.Remove(); maintainPosOriginIndicator?.Remove(); steeringIndicator?.Remove(); diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Power/PowerContainer.cs index 3c2f9a3e2..cb6f22c5b 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Power/PowerContainer.cs @@ -80,6 +80,7 @@ namespace Barotrauma.Items.Components public override void OnItemLoaded() { + base.OnItemLoaded(); if (rechargeSpeedSlider != null) { rechargeSpeedSlider.BarScroll = rechargeSpeed / MaxRechargeSpeed; diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Power/PowerTransfer.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Power/PowerTransfer.cs index 272dfe94f..78abdac83 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Power/PowerTransfer.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Power/PowerTransfer.cs @@ -1,5 +1,6 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using System; using System.Xml.Linq; namespace Barotrauma.Items.Components @@ -46,19 +47,26 @@ namespace Barotrauma.Items.Components new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), textContainer.RectTransform), "", textColor: Color.LightGreen) { ToolTip = TextManager.Get("PowerTransferTipPower"), - TextGetter = () => { return powerStr.Replace("[power]", ((int)(-currPowerConsumption)).ToString()); } + TextGetter = () => { return powerStr.Replace("[power]", ((int)Math.Round(-currPowerConsumption)).ToString()); } }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), textContainer.RectTransform), TextManager.Get("PowerTransferLoadLabel"), font: GUI.LargeFont) { ToolTip = TextManager.Get("PowerTransferTipLoad") + }; string loadStr = TextManager.Get("PowerTransferLoad"); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), textContainer.RectTransform), "", textColor: Color.LightBlue) { ToolTip = TextManager.Get("PowerTransferTipLoad"), - TextGetter = () => { return loadStr.Replace("[load]", ((int)(powerLoad)).ToString()); } + TextGetter = () => + { + return loadStr.Replace("[load]", + this is RelayComponent relay ? + ((int)Math.Round(relay.DisplayLoad)).ToString() : + ((int)Math.Round(powerLoad)).ToString()); + } }; } diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Signal/Wire.cs index 0f47fbbf6..cddda1212 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Signal/Wire.cs @@ -5,6 +5,7 @@ using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; using System.Linq; +using System.Xml.Linq; namespace Barotrauma.Items.Components { @@ -12,32 +13,34 @@ namespace Barotrauma.Items.Components { partial class WireSection { - public void Draw(SpriteBatch spriteBatch, Color color, Vector2 offset, float depth, float width = 0.3f) + public void Draw(SpriteBatch spriteBatch, Wire wire, Color color, Vector2 offset, float depth, float width = 0.3f) { - spriteBatch.Draw(wireSprite.Texture, + spriteBatch.Draw(wire.wireSprite.Texture, new Vector2(start.X + offset.X, -(start.Y + offset.Y)), null, color, -angle, - new Vector2(0.0f, wireSprite.size.Y / 2.0f), - new Vector2(length / wireSprite.Texture.Width, width), + new Vector2(0.0f, wire.wireSprite.size.Y / 2.0f), + new Vector2(length / wire.wireSprite.Texture.Width, width), SpriteEffects.None, depth); } - public static void Draw(SpriteBatch spriteBatch, Vector2 start, Vector2 end, Color color, float depth, float width = 0.3f) + public static void Draw(SpriteBatch spriteBatch, Wire wire, Vector2 start, Vector2 end, Color color, float depth, float width = 0.3f) { start.Y = -start.Y; end.Y = -end.Y; - - spriteBatch.Draw(wireSprite.Texture, + + spriteBatch.Draw(wire.wireSprite.Texture, start, null, color, MathUtils.VectorToAngle(end - start), - new Vector2(0.0f, wireSprite.size.Y / 2.0f), - new Vector2((Vector2.Distance(start, end)) / wireSprite.Texture.Width, width), + new Vector2(0.0f, wire.wireSprite.size.Y / 2.0f), + new Vector2((Vector2.Distance(start, end)) / wire.wireSprite.Texture.Width, width), SpriteEffects.None, depth); } } - private static Sprite wireSprite; + private static Sprite defaultWireSprite; + private Sprite overrideSprite; + private Sprite wireSprite; private static Wire draggingWire; private static int? selectedNodeIndex; @@ -48,6 +51,28 @@ namespace Barotrauma.Items.Components get { return sectionExtents; } } + partial void InitProjSpecific(XElement element) + { + if (defaultWireSprite == null) + { + defaultWireSprite = new Sprite("Content/Items/wireHorizontal.png", new Vector2(0.5f, 0.5f)) + { + Depth = 0.85f + }; + } + + foreach (XElement subElement in element.Elements()) + { + if (subElement.Name.ToString().ToLowerInvariant() == "wiresprite") + { + overrideSprite = new Sprite(subElement); + break; + } + } + + wireSprite = overrideSprite ?? defaultWireSprite; + } + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) { if (sections.Count == 0 && !IsActive || Hidden) @@ -75,20 +100,20 @@ namespace Barotrauma.Items.Components { foreach (WireSection section in sections) { - section.Draw(spriteBatch, Color.Gold, drawOffset, depth + 0.00001f, 0.7f); + section.Draw(spriteBatch, this, Color.Gold, drawOffset, depth + 0.00001f, 0.7f); } } else if (item.IsSelected) { foreach (WireSection section in sections) { - section.Draw(spriteBatch, Color.Red, drawOffset, depth + 0.00001f, 0.7f); + section.Draw(spriteBatch, this, Color.Red, drawOffset, depth + 0.00001f, 0.7f); } } foreach (WireSection section in sections) { - section.Draw(spriteBatch, item.Color, drawOffset, depth, 0.3f); + section.Draw(spriteBatch, this, item.Color, drawOffset, depth, 0.3f); } if (nodes.Count > 0) @@ -101,7 +126,8 @@ namespace Barotrauma.Items.Components if (IsActive && Vector2.Distance(newNodePos, nodes[nodes.Count - 1]) > nodeDistance) { WireSection.Draw( - spriteBatch, + spriteBatch, + this, new Vector2(nodes[nodes.Count - 1].X, nodes[nodes.Count - 1].Y) + drawOffset, new Vector2(newNodePos.X, newNodePos.Y) + drawOffset, item.Color * 0.5f, @@ -141,12 +167,12 @@ namespace Barotrauma.Items.Components Vector2 endPos = start + new Vector2((float)Math.Sin(angle), -(float)Math.Cos(angle)) * 50.0f; WireSection.Draw( - spriteBatch, + spriteBatch, this, start, endPos, Color.Orange, depth + 0.00001f, 0.2f); WireSection.Draw( - spriteBatch, + spriteBatch, this, start, start + (endPos - start) * 0.7f, item.Color, depth, 0.3f); } diff --git a/Barotrauma/BarotraumaClient/Source/Items/Inventory.cs b/Barotrauma/BarotraumaClient/Source/Items/Inventory.cs index 2ca75ce47..9f5425e5b 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Inventory.cs @@ -993,7 +993,7 @@ namespace Barotrauma Color.Lerp(Color.Red, Color.Green, item.Condition / item.MaxCondition) * 0.8f, true); } - if (itemContainer != null) + if (itemContainer != null && itemContainer.ShowContainedStateIndicator) { float containedState = 0.0f; if (itemContainer.ShowConditionInContainedStateIndicator) @@ -1156,7 +1156,7 @@ namespace Barotrauma private void ApplyReceivedState() { - if (receivedItemIDs == null) return; + if (receivedItemIDs == null || (Owner != null && Owner.Removed)) { return; } for (int i = 0; i < capacity; i++) { @@ -1171,7 +1171,7 @@ namespace Barotrauma { if (receivedItemIDs[i] > 0) { - if (!(Entity.FindEntityByID(receivedItemIDs[i]) is Item item) || Items[i] == item) continue; + if (!(Entity.FindEntityByID(receivedItemIDs[i]) is Item item) || Items[i] == item) { continue; } TryPutItem(item, i, true, true, null, false); for (int j = 0; j < capacity; j++) diff --git a/Barotrauma/BarotraumaClient/Source/Items/Item.cs b/Barotrauma/BarotraumaClient/Source/Items/Item.cs index 13a582b24..7af86c154 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Item.cs @@ -19,7 +19,7 @@ namespace Barotrauma private readonly List positionBuffer = new List(); - private List activeHUDs = new List(); + private readonly List activeHUDs = new List(); public IEnumerable ActiveHUDs => activeHUDs; @@ -230,9 +230,6 @@ namespace Barotrauma if (body == null) { - bool flipHorizontal = (SpriteEffects & SpriteEffects.FlipHorizontally) != 0; - bool flipVertical = (SpriteEffects & SpriteEffects.FlipVertically) != 0; - if (prefab.ResizeHorizontal || prefab.ResizeVertical) { activeSprite.DrawTiled(spriteBatch, new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)), new Vector2(rect.Width, rect.Height), color: color, @@ -570,7 +567,7 @@ namespace Barotrauma } else { - if (ic.requiredItems.Count == 0 && SerializableProperty.GetProperties(ic).Count == 0) continue; + if (ic.requiredItems.Count == 0 && ic.DisabledRequiredItems.Count == 0 && SerializableProperty.GetProperties(ic).Count == 0) continue; } var componentEditor = new SerializableEntityEditor(listBox.Content.RectTransform, ic, inGame, showName: !inGame); @@ -582,37 +579,44 @@ namespace Barotrauma continue; } + List requiredItems = new List(); foreach (var kvp in ic.requiredItems) { foreach (RelatedItem relatedItem in kvp.Value) { - var textBlock = new GUITextBlock(new RectTransform(new Point(editingHUD.Rect.Width, heightScaled)), - relatedItem.Type.ToString() + " required", font: GUI.SmallFont) - { - Padding = new Vector4(10.0f, 0.0f, 10.0f, 0.0f) - }; - componentEditor.AddCustomContent(textBlock, 1); - - GUITextBox namesBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), textBlock.RectTransform, Anchor.CenterRight)) - { - Font = GUI.SmallFont, - Text = relatedItem.JoinedIdentifiers - }; - - namesBox.OnDeselected += (textBox, key) => - { - relatedItem.JoinedIdentifiers = textBox.Text; - textBox.Text = relatedItem.JoinedIdentifiers; - }; - - namesBox.OnEnterPressed += (textBox, text) => - { - relatedItem.JoinedIdentifiers = text; - textBox.Text = relatedItem.JoinedIdentifiers; - return true; - }; + requiredItems.Add(relatedItem); } } + requiredItems.AddRange(ic.DisabledRequiredItems); + + foreach (RelatedItem relatedItem in requiredItems) + { + var textBlock = new GUITextBlock(new RectTransform(new Point(editingHUD.Rect.Width, heightScaled)), + relatedItem.Type.ToString() + " required", font: GUI.SmallFont) + { + Padding = new Vector4(10.0f, 0.0f, 10.0f, 0.0f) + }; + componentEditor.AddCustomContent(textBlock, 1); + + GUITextBox namesBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), textBlock.RectTransform, Anchor.CenterRight)) + { + Font = GUI.SmallFont, + Text = relatedItem.JoinedIdentifiers + }; + + namesBox.OnDeselected += (textBox, key) => + { + relatedItem.JoinedIdentifiers = textBox.Text; + textBox.Text = relatedItem.JoinedIdentifiers; + }; + + namesBox.OnEnterPressed += (textBox, text) => + { + relatedItem.JoinedIdentifiers = text; + textBox.Text = relatedItem.JoinedIdentifiers; + return true; + }; + } ic.CreateEditingHUD(componentEditor); componentEditor.Recalculate(); @@ -782,7 +786,7 @@ namespace Barotrauma } } - List texts = new List(); + readonly List texts = new List(); public List GetHUDTexts(Character character) { texts.Clear(); diff --git a/Barotrauma/BarotraumaClient/Source/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/Source/Map/Lights/LightManager.cs index 97146d94a..40acaf548 100644 --- a/Barotrauma/BarotraumaClient/Source/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/Source/Map/Lights/LightManager.cs @@ -216,7 +216,7 @@ namespace Barotrauma.Lights if (GameMain.Config.SpecularityEnabled) { - UpdateSpecularMap(graphics, spriteBatch, spriteBatchTransform, cam, backgroundObstructor); + //UpdateSpecularMap(graphics, spriteBatch, spriteBatchTransform, cam, backgroundObstructor); } graphics.SetRenderTarget(LightMap); @@ -302,19 +302,38 @@ namespace Barotrauma.Lights //draw characters to obstruct the highlighted items/characters and light sprites //--------------------------------------------------------------------------------------------------- - spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.AlphaBlend, transformMatrix: spriteBatchTransform); + + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, effect: SolidColorEffect, transformMatrix: spriteBatchTransform); foreach (Character character in Character.CharacterList) { if (character.CurrentHull == null || !character.Enabled) continue; - if (Character.Controlled?.FocusedCharacter == character) continue; + if (Character.Controlled?.FocusedCharacter == character) continue; foreach (Limb limb in character.AnimController.Limbs) { + if (limb.DeformSprite != null) continue; limb.Draw(spriteBatch, cam, Color.Black); } } spriteBatch.End(); - graphics.BlendState = BlendState.Additive; + DeformableSprite.Effect.CurrentTechnique = DeformableSprite.Effect.Techniques["DeformShaderSolidColor"]; + DeformableSprite.Effect.Parameters["solidColor"].SetValue(Color.Black.ToVector4()); + DeformableSprite.Effect.CurrentTechnique.Passes[0].Apply(); + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, transformMatrix: spriteBatchTransform); + foreach (Character character in Character.CharacterList) + { + if (character.CurrentHull == null || !character.Enabled) continue; + if (Character.Controlled?.FocusedCharacter == character) continue; + foreach (Limb limb in character.AnimController.Limbs) + { + if (limb.DeformSprite == null) continue; + limb.Draw(spriteBatch, cam, Color.Black); + } + } + spriteBatch.End(); + DeformableSprite.Effect.CurrentTechnique = DeformableSprite.Effect.Techniques["DeformShader"]; + graphics.BlendState = BlendState.Additive; + //draw the actual light volumes, additive particles, hull ambient lights and the halo around the player //--------------------------------------------------------------------------------------------------- spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive, transformMatrix: spriteBatchTransform); @@ -372,10 +391,10 @@ namespace Barotrauma.Lights if (GameMain.Config.SpecularityEnabled) { - spriteBatch.Begin(blendState: CustomBlendStates.Multiplicative); + /*spriteBatch.Begin(blendState: CustomBlendStates.Multiplicative); spriteBatch.Draw(SpecularMap, Vector2.Zero, Color.White); //spriteBatch.Draw(SpecularMap, Vector2.Zero, Color.White); - spriteBatch.End(); + spriteBatch.End();*/ } //draw the actual light volumes, additive particles, hull ambient lights and the halo around the player diff --git a/Barotrauma/BarotraumaClient/Source/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/Source/Map/Map/Map.cs index 270dfe590..e62c283e7 100644 --- a/Barotrauma/BarotraumaClient/Source/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/Source/Map/Map/Map.cs @@ -47,6 +47,7 @@ namespace Barotrauma private Vector2 drawOffset; private Vector2 drawOffsetNoise; + private float subReticleAnimState; private float targetReticleAnimState; private Vector2 subReticlePosition; @@ -57,6 +58,9 @@ namespace Barotrauma private MapTile[,] mapTiles; private bool messageBoxOpen; + + public Vector2 CenterOffset; + #if DEBUG private GUIComponent editor; @@ -316,7 +320,7 @@ namespace Barotrauma hudOpenState = Math.Min(hudOpenState + deltaTime, 0.75f + (float)Math.Sin(Timing.TotalTime * 3.0f) * 0.25f); - Vector2 rectCenter = new Vector2(rect.Center.X, rect.Center.Y); + Vector2 rectCenter = new Vector2(rect.Center.X, rect.Center.Y) + CenterOffset; float closestDist = 0.0f; highlightedLocation = null; @@ -327,7 +331,7 @@ namespace Barotrauma Location location = Locations[i]; Vector2 pos = rectCenter + (location.MapPosition + drawOffset) * zoom; - if (!rect.Contains(pos)) continue; + if (!rect.Contains(pos)) { continue; } float iconScale = MapGenerationParams.Instance.LocationIconSize / location.Type.Sprite.size.X; @@ -348,28 +352,6 @@ namespace Barotrauma } } - foreach (LocationConnection connection in connections) - { - if (highlightedLocation != CurrentLocation && - connection.Locations.Contains(highlightedLocation) && connection.Locations.Contains(CurrentLocation)) - { - if (PlayerInput.LeftButtonClicked() && - SelectedLocation != highlightedLocation && highlightedLocation != null) - { - //clients aren't allowed to select the location without a permission - if (GameMain.Client == null || GameMain.Client.HasPermission(Networking.ClientPermissions.ManageCampaign)) - { - SelectedConnection = connection; - SelectedLocation = highlightedLocation; - targetReticleAnimState = 0.0f; - - OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection); - GameMain.Client?.SendCampaignState(); - } - } - } - } - if (GUI.KeyboardDispatcher.Subscriber == null) { float moveSpeed = 1000.0f; @@ -383,6 +365,28 @@ namespace Barotrauma if (GUI.MouseOn == mapContainer) { + foreach (LocationConnection connection in connections) + { + if (highlightedLocation != CurrentLocation && + connection.Locations.Contains(highlightedLocation) && connection.Locations.Contains(CurrentLocation)) + { + if (PlayerInput.LeftButtonClicked() && + SelectedLocation != highlightedLocation && highlightedLocation != null) + { + //clients aren't allowed to select the location without a permission + if (GameMain.Client == null || GameMain.Client.HasPermission(Networking.ClientPermissions.ManageCampaign)) + { + SelectedConnection = connection; + SelectedLocation = highlightedLocation; + targetReticleAnimState = 0.0f; + + OnLocationSelected?.Invoke(SelectedLocation, SelectedConnection); + GameMain.Client?.SendCampaignState(); + } + } + } + } + zoom += PlayerInput.ScrollWheelSpeed / 1000.0f; zoom = MathHelper.Clamp(zoom, 1.0f, 4.0f); @@ -425,12 +429,12 @@ namespace Barotrauma Vector2 viewOffset = drawOffset + drawOffsetNoise; - Vector2 rectCenter = new Vector2(rect.Center.X, rect.Center.Y); + Vector2 rectCenter = new Vector2(rect.Center.X, rect.Center.Y) + CenterOffset; Rectangle prevScissorRect = GameMain.Instance.GraphicsDevice.ScissorRectangle; spriteBatch.End(); spriteBatch.GraphicsDevice.ScissorRectangle = Rectangle.Intersect(prevScissorRect, rect); - spriteBatch.Begin(SpriteSortMode.Deferred, rasterizerState: GameMain.ScissorTestEnable); + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); for (int x = 0; x < mapTiles.GetLength(0); x++) { @@ -662,15 +666,15 @@ namespace Barotrauma Vector2 size = GUI.LargeFont.MeasureString(location.Name); GUI.Style.GetComponentStyle("OuterGlow").Sprites[GUIComponent.ComponentState.None][0].Draw( spriteBatch, new Rectangle((int)pos.X - 30, (int)pos.Y, (int)size.X + 60, (int)(size.Y + 25 * GUI.Scale)), Color.Black * hudOpenState * 0.7f); - GUI.DrawString(spriteBatch, pos, + GUI.DrawString(spriteBatch, pos, location.Name, Color.White * hudOpenState * 1.5f, font: GUI.LargeFont); - GUI.DrawString(spriteBatch, pos + Vector2.UnitY * 25 * GUI.Scale, + GUI.DrawString(spriteBatch, pos + Vector2.UnitY * 25 * GUI.Scale, location.Type.Name, Color.White * hudOpenState * 1.5f); } - - GameMain.Instance.GraphicsDevice.ScissorRectangle = prevScissorRect; + spriteBatch.End(); - spriteBatch.Begin(SpriteSortMode.Deferred); + GameMain.Instance.GraphicsDevice.ScissorRectangle = prevScissorRect; + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); } private IEnumerable WaitForMessageBoxClosed(GUIMessageBox box) @@ -687,11 +691,13 @@ namespace Barotrauma private void DrawDecorativeHUD(SpriteBatch spriteBatch, Rectangle rect) { spriteBatch.End(); - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive, null, null, GameMain.ScissorTestEnable); - + spriteBatch.Begin(SpriteSortMode.Deferred, blendState: BlendState.Additive, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); + + Vector2 rectCenter = rect.Center.ToVector2() + CenterOffset; + if (generationParams.ShowOverlay) { - Vector2 mapCenter = rect.Center.ToVector2() + (new Vector2(size, size) / 2 + drawOffset + drawOffsetNoise) * zoom; + Vector2 mapCenter = rectCenter + (new Vector2(size, size) / 2 + drawOffset + drawOffsetNoise) * zoom; Vector2 centerDiff = CurrentLocation.MapPosition - new Vector2(size) / 2; int currentZone = (int)Math.Floor((centerDiff.Length() / (size * 0.5f) * generationParams.DifficultyZones)); for (int i = 0; i < generationParams.DifficultyZones; i++) @@ -754,21 +760,21 @@ namespace Barotrauma //reticles generationParams.ReticleLarge.Draw(spriteBatch, (int)(subReticleAnimState * generationParams.ReticleLarge.FrameCount), - rect.Center.ToVector2() + (subReticlePosition + drawOffset - drawOffsetNoise * 2) * zoom, Color.White, + rectCenter + (subReticlePosition + drawOffset - drawOffsetNoise * 2) * zoom, Color.White, generationParams.ReticleLarge.Origin, 0, Vector2.One * (float)Math.Sqrt(zoom) * 0.4f); generationParams.ReticleMedium.Draw(spriteBatch, (int)(subReticleAnimState * generationParams.ReticleMedium.FrameCount), - rect.Center.ToVector2() + (subReticlePosition + drawOffset - drawOffsetNoise) * zoom, Color.White, + rectCenter + (subReticlePosition + drawOffset - drawOffsetNoise) * zoom, Color.White, generationParams.ReticleMedium.Origin, 0, new Vector2(1.0f, 0.7f) * (float)Math.Sqrt(zoom) * 0.4f); if (SelectedLocation != null) { generationParams.ReticleSmall.Draw(spriteBatch, (int)(targetReticleAnimState * generationParams.ReticleSmall.FrameCount), - rect.Center.ToVector2() + (SelectedLocation.MapPosition + drawOffset + drawOffsetNoise * 2) * zoom, Color.White, + rectCenter + (SelectedLocation.MapPosition + drawOffset + drawOffsetNoise * 2) * zoom, Color.White, generationParams.ReticleSmall.Origin, 0, new Vector2(1.0f, 0.7f) * (float)Math.Sqrt(zoom) * 0.4f); } spriteBatch.End(); - spriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, GameMain.ScissorTestEnable); + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); } private void UpdateMapAnim(MapAnim anim, float deltaTime) @@ -788,8 +794,6 @@ namespace Barotrauma anim.StartPos = (anim.StartLocation == null) ? -drawOffset : anim.StartLocation.MapPosition; - - anim.Timer = Math.Min(anim.Timer + deltaTime, anim.Duration); float t = anim.Duration <= 0.0f ? 1.0f : Math.Max(anim.Timer / anim.Duration, 0.0f); drawOffset = -Vector2.SmoothStep(anim.StartPos.Value, anim.EndLocation.MapPosition, t); diff --git a/Barotrauma/BarotraumaClient/Source/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/Source/Map/MapEntity.cs index c0edd053d..3d0e1b5d5 100644 --- a/Barotrauma/BarotraumaClient/Source/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/Source/Map/MapEntity.cs @@ -91,9 +91,7 @@ namespace Barotrauma } public virtual void Draw(SpriteBatch spriteBatch, bool editing, bool back = true) { } - - public virtual void DrawDamage(SpriteBatch spriteBatch, Effect damageEffect, bool editing) { } - + /// /// Update the selection logic in submarine editor /// diff --git a/Barotrauma/BarotraumaClient/Source/Map/Structure.cs b/Barotrauma/BarotraumaClient/Source/Map/Structure.cs index b4f2bf2d4..8af490237 100644 --- a/Barotrauma/BarotraumaClient/Source/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/Source/Map/Structure.cs @@ -170,11 +170,18 @@ namespace Barotrauma Draw(spriteBatch, editing, back, null); } - public override void DrawDamage(SpriteBatch spriteBatch, Effect damageEffect, bool editing) + public void DrawDamage(SpriteBatch spriteBatch, Effect damageEffect, bool editing) { Draw(spriteBatch, editing, false, damageEffect); } + public float GetDrawDepth() + { + float depth = SpriteDepthOverrideIsSet ? SpriteOverrideDepth : prefab.sprite.Depth; + depth -= (ID % 255) * 0.000001f; + return depth; + } + private void Draw(SpriteBatch spriteBatch, bool editing, bool back = true, Effect damageEffect = null) { if (prefab.sprite == null) return; @@ -202,8 +209,7 @@ namespace Barotrauma Vector2 drawOffset = Submarine == null ? Vector2.Zero : Submarine.DrawPosition; - float depth = SpriteDepthOverrideIsSet ? SpriteOverrideDepth : prefab.sprite.Depth; - depth -= (ID % 255) * 0.000001f; + float depth = GetDrawDepth(); Vector2 textureOffset = this.textureOffset; if (FlippedX) textureOffset.X = -textureOffset.X; @@ -247,7 +253,7 @@ namespace Barotrauma color: color, textureScale: TextureScale * Scale, startOffset: backGroundOffset, - depth: Math.Max(Prefab.BackgroundSprite.Depth, depth + 0.000001f)); + depth: Math.Max(Prefab.BackgroundSprite.Depth + (ID % 255) * 0.000001f, depth + 0.000001f)); if (UseDropShadow) { diff --git a/Barotrauma/BarotraumaClient/Source/Map/Submarine.cs b/Barotrauma/BarotraumaClient/Source/Map/Submarine.cs index 9d859adf5..043255668 100644 --- a/Barotrauma/BarotraumaClient/Source/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/Source/Map/Submarine.cs @@ -228,20 +228,38 @@ namespace Barotrauma public static float DamageEffectCutoff; public static Color DamageEffectColor; + private static readonly List depthSortedDamageable = new List(); public static void DrawDamageable(SpriteBatch spriteBatch, Effect damageEffect, bool editing = false) { var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.mapEntityList; + depthSortedDamageable.Clear(); + + //insertion sort according to draw depth foreach (MapEntity e in entitiesToRender) { - if (e.DrawDamageEffect) - e.DrawDamage(spriteBatch, damageEffect, editing); + if (e is Structure structure && structure.DrawDamageEffect) + { + float drawDepth = structure.GetDrawDepth(); + int i = 0; + while (i < depthSortedDamageable.Count) + { + float otherDrawDepth = depthSortedDamageable[i].GetDrawDepth(); + if (otherDrawDepth < drawDepth) { break; } + i++; + } + depthSortedDamageable.Insert(i, structure); + } + } + + foreach (Structure s in depthSortedDamageable) + { + s.DrawDamage(spriteBatch, damageEffect, editing); } if (damageEffect != null) { damageEffect.Parameters["aCutoff"].SetValue(0.0f); damageEffect.Parameters["cCutoff"].SetValue(0.0f); - DamageEffectCutoff = 0.0f; } } @@ -274,18 +292,6 @@ namespace Barotrauma return MainSub.SaveAs(filePath, previewImage); } - public void CreatePreviewWindow(GUIMessageBox messageBox) - { - var background = new GUIButton(new RectTransform(Vector2.One, messageBox.RectTransform), style: "GUIBackgroundBlocker") - { - OnClicked = (btn, userdata) => { if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) messageBox.Close(); return true; } - }; - background.RectTransform.SetAsFirstChild(); - - var holder = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.85f), messageBox.Content.RectTransform), style: null); - CreatePreviewWindow(holder); - } - public void CreatePreviewWindow(GUIComponent parent) { var upperPart = new GUILayoutGroup(new RectTransform(new Vector2(1, 0.5f), parent.RectTransform, Anchor.Center, Pivot.BottomCenter)); @@ -297,7 +303,7 @@ namespace Barotrauma if (PreviewImage == null) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 1), upperPart.RectTransform), TextManager.Get("SubPreviewImageNotFound")); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 1), upperPart.RectTransform), TextManager.Get(SavedSubmarines.Contains(this) ? "SubPreviewImageNotFound" : "SubNotDownloaded")); } else { @@ -310,49 +316,54 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(1, 0), descriptionBox.Content.RectTransform), Name, font: GUI.LargeFont, wrap: true) { ForceUpperCase = true, CanBeFocused = false }; + float leftPanelWidth = 0.6f; + float rightPanelWidth = 0.4f / leftPanelWidth; + + ScalableFont font = descriptionBox.Rect.Width < 350 ? GUI.SmallFont : GUI.Font; + Vector2 realWorldDimensions = Dimensions * Physics.DisplayToRealWorldRatio; if (realWorldDimensions != Vector2.Zero) { string dimensionsStr = TextManager.GetWithVariables("DimensionsFormat", new string[2] { "[width]", "[height]" }, new string[2] { ((int)realWorldDimensions.X).ToString(), ((int)realWorldDimensions.Y).ToString() }); - var dimensionsText = new GUITextBlock(new RectTransform(new Vector2(1, 0), descriptionBox.Content.RectTransform), - TextManager.Get("Dimensions"), textAlignment: Alignment.TopLeft, font: GUI.Font, wrap: true) + var dimensionsText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), descriptionBox.Content.RectTransform), + TextManager.Get("Dimensions"), textAlignment: Alignment.TopLeft, font: font, wrap: true) { CanBeFocused = false }; - new GUITextBlock(new RectTransform(new Vector2(0.45f, 0.0f), dimensionsText.RectTransform, Anchor.TopRight), - dimensionsStr, textAlignment: Alignment.TopLeft, font: GUI.Font, wrap: true) + new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), dimensionsText.RectTransform, Anchor.TopRight, Pivot.TopLeft), + dimensionsStr, textAlignment: Alignment.TopLeft, font: font, wrap: true) { CanBeFocused = false }; dimensionsText.RectTransform.MinSize = new Point(0, dimensionsText.Children.First().Rect.Height); } if (RecommendedCrewSizeMax > 0) { - var crewSizeText = new GUITextBlock(new RectTransform(new Vector2(1, 0), descriptionBox.Content.RectTransform), - TextManager.Get("RecommendedCrewSize"), textAlignment: Alignment.TopLeft, font: GUI.Font, wrap: true) + var crewSizeText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), descriptionBox.Content.RectTransform), + TextManager.Get("RecommendedCrewSize"), textAlignment: Alignment.TopLeft, font: font, wrap: true) { CanBeFocused = false }; - new GUITextBlock(new RectTransform(new Vector2(0.45f, 0.0f), crewSizeText.RectTransform, Anchor.TopRight), - RecommendedCrewSizeMin + " - " + RecommendedCrewSizeMax, textAlignment: Alignment.TopLeft, font: GUI.Font, wrap: true) + new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), crewSizeText.RectTransform, Anchor.TopRight, Pivot.TopLeft), + RecommendedCrewSizeMin + " - " + RecommendedCrewSizeMax, textAlignment: Alignment.TopLeft, font: font, wrap: true) { CanBeFocused = false }; crewSizeText.RectTransform.MinSize = new Point(0, crewSizeText.Children.First().Rect.Height); } if (!string.IsNullOrEmpty(RecommendedCrewExperience)) { - var crewExperienceText = new GUITextBlock(new RectTransform(new Vector2(1, 0), descriptionBox.Content.RectTransform), - TextManager.Get("RecommendedCrewExperience"), textAlignment: Alignment.TopLeft, font: GUI.Font, wrap: true) + var crewExperienceText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), descriptionBox.Content.RectTransform), + TextManager.Get("RecommendedCrewExperience"), textAlignment: Alignment.TopLeft, font: font, wrap: true) { CanBeFocused = false }; - new GUITextBlock(new RectTransform(new Vector2(0.45f, 0.0f), crewExperienceText.RectTransform, Anchor.TopRight), - TextManager.Get(RecommendedCrewExperience), textAlignment: Alignment.TopLeft, font: GUI.Font, wrap: true) + new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), crewExperienceText.RectTransform, Anchor.TopRight, Pivot.TopLeft), + TextManager.Get(RecommendedCrewExperience), textAlignment: Alignment.TopLeft, font: font, wrap: true) { CanBeFocused = false }; crewExperienceText.RectTransform.MinSize = new Point(0, crewExperienceText.Children.First().Rect.Height); } if (RequiredContentPackages.Any()) { - var contentPackagesText = new GUITextBlock(new RectTransform(new Vector2(1, 0), descriptionBox.Content.RectTransform), - TextManager.Get("RequiredContentPackages"), textAlignment: Alignment.TopLeft, font: GUI.Font) + var contentPackagesText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), descriptionBox.Content.RectTransform), + TextManager.Get("RequiredContentPackages"), textAlignment: Alignment.TopLeft, font: font) { CanBeFocused = false }; - new GUITextBlock(new RectTransform(new Vector2(0.45f, 0.0f), contentPackagesText.RectTransform, Anchor.TopRight), - string.Join(", ", RequiredContentPackages), textAlignment: Alignment.TopLeft, font: GUI.Font, wrap: true) + new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), contentPackagesText.RectTransform, Anchor.TopRight, Pivot.TopLeft), + string.Join(", ", RequiredContentPackages), textAlignment: Alignment.TopLeft, font: font, wrap: true) { CanBeFocused = false }; contentPackagesText.RectTransform.MinSize = new Point(0, contentPackagesText.Children.First().Rect.Height); } @@ -362,13 +373,13 @@ namespace Barotrauma //space new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), descriptionBox.Content.RectTransform), style: null); - if (Description.Length != 0) + if (!string.IsNullOrEmpty(Description)) { new GUITextBlock(new RectTransform(new Vector2(1, 0), descriptionBox.Content.RectTransform), TextManager.Get("SaveSubDialogDescription", fallBackTag: "WorkshopItemDescription"), font: GUI.Font, wrap: true) { CanBeFocused = false, ForceUpperCase = true }; } - new GUITextBlock(new RectTransform(new Vector2(1, 0), descriptionBox.Content.RectTransform), Description, font: GUI.Font, wrap: true) + new GUITextBlock(new RectTransform(new Vector2(1, 0), descriptionBox.Content.RectTransform), Description, font: font, wrap: true) { CanBeFocused = false }; diff --git a/Barotrauma/BarotraumaClient/Source/Media/Video.cs b/Barotrauma/BarotraumaClient/Source/Media/Video.cs index feaba702d..bcc56603e 100644 --- a/Barotrauma/BarotraumaClient/Source/Media/Video.cs +++ b/Barotrauma/BarotraumaClient/Source/Media/Video.cs @@ -108,7 +108,13 @@ namespace Barotrauma.Media public uint Width { get; private set; } public uint Height { get; private set; } - public Video(GraphicsDevice graphicsDevice,SoundManager soundManager,string filename,uint width,uint height) + public float AudioGain + { + get { return sound == null ? 0.0f : sound.BaseGain; } + set { if (sound != null) { sound.BaseGain = value; } } + } + + public Video(GraphicsDevice graphicsDevice, SoundManager soundManager, string filename, uint width, uint height) { Init(); diff --git a/Barotrauma/BarotraumaClient/Source/Networking/Client.cs b/Barotrauma/BarotraumaClient/Source/Networking/Client.cs index 2d6617091..ae9a597bc 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/Client.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/Client.cs @@ -9,6 +9,7 @@ namespace Barotrauma.Networking struct TempClient { public string Name; + public string PreferredJob; public UInt16 NameID; public UInt64 SteamID; public byte ID; diff --git a/Barotrauma/BarotraumaClient/Source/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaClient/Source/Networking/EntitySpawner.cs index d414da6a2..9ab68965f 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/EntitySpawner.cs @@ -31,7 +31,7 @@ namespace Barotrauma Item.ReadSpawnData(message, true); break; case (byte)SpawnableType.Character: - Character.ReadSpawnData(message, true); + Character.ReadSpawnData(message); break; default: DebugConsole.ThrowError("Received invalid entity spawn message (unknown spawnable type)"); diff --git a/Barotrauma/BarotraumaClient/Source/Networking/FileTransfer/FileReceiver.cs b/Barotrauma/BarotraumaClient/Source/Networking/FileTransfer/FileReceiver.cs index 662795f19..e7a50fc84 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/FileTransfer/FileReceiver.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/FileTransfer/FileReceiver.cs @@ -164,8 +164,8 @@ namespace Barotrauma.Networking private Dictionary downloadFolders = new Dictionary() { - { FileTransferType.Submarine, "Submarines/Downloaded" }, - { FileTransferType.CampaignSave, "Data/Saves/Multiplayer" } + { FileTransferType.Submarine, SaveUtil.SubmarineDownloadFolder }, + { FileTransferType.CampaignSave, SaveUtil.CampaignDownloadFolder } }; public List ActiveTransfers diff --git a/Barotrauma/BarotraumaClient/Source/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/Source/Networking/GameClient.cs index 83fdc588f..43c3b637f 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/GameClient.cs @@ -29,8 +29,14 @@ namespace Barotrauma.Networking public void SetName(string value) { + value = value.Replace(":", "").Replace(";", ""); if (string.IsNullOrEmpty(value)) { return; } - name = value.Replace(":", "").Replace(";", ""); + name = value; + nameId++; + } + + public void ForceNameAndJobUpdate() + { nameId++; } @@ -265,9 +271,9 @@ namespace Barotrauma.Networking private void ConnectToServer(object endpoint, string hostName) { chatBox.InputBox.Enabled = false; - if (GameMain.NetLobbyScreen?.TextBox != null) + if (GameMain.NetLobbyScreen?.ChatInput != null) { - GameMain.NetLobbyScreen.TextBox.Enabled = false; + GameMain.NetLobbyScreen.ChatInput.Enabled = false; } serverName = hostName; @@ -334,7 +340,7 @@ namespace Barotrauma.Networking { SteamManager.Instance.User.ClearRichPresence(); SteamManager.Instance.User.SetRichPresence("status", "Playing on " + serverName); - SteamManager.Instance.User.SetRichPresence("connect", "-connect \"" + serverName.Replace("\"","\\\"") + "\" " + serverEndpoint); + SteamManager.Instance.User.SetRichPresence("connect", "-connect \"" + serverName.Replace("\"", "\\\"") + "\" " + serverEndpoint); } canStart = true; @@ -348,9 +354,9 @@ namespace Barotrauma.Networking } chatBox.InputBox.Enabled = true; - if (GameMain.NetLobbyScreen?.TextBox != null) + if (GameMain.NetLobbyScreen?.ChatInput != null) { - GameMain.NetLobbyScreen.TextBox.Enabled = true; + GameMain.NetLobbyScreen.ChatInput.Enabled = true; } }; clientPeer.OnRequestPassword = (int salt, int retries) => @@ -370,9 +376,9 @@ namespace Barotrauma.Networking DebugConsole.ThrowError("Couldn't connect to " + endpoint.ToString() + ". Error message: " + e.Message); Disconnect(); chatBox.InputBox.Enabled = true; - if (GameMain.NetLobbyScreen?.TextBox != null) + if (GameMain.NetLobbyScreen?.ChatInput != null) { - GameMain.NetLobbyScreen.TextBox.Enabled = true; + GameMain.NetLobbyScreen.ChatInput.Enabled = true; } GameMain.ServerListScreen.Select(); return; @@ -630,11 +636,14 @@ namespace Barotrauma.Networking if (IsServerOwner && connected && !connectCancelled) { - if (GameMain.ServerChildProcess?.HasExited ?? true) + if (GameMain.WindowActive) { - Disconnect(); - var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), TextManager.Get("ServerProcessClosed")); - msgBox.Buttons[0].OnClicked += ReturnToPreviousMenu; + if (GameMain.ServerChildProcess?.HasExited ?? true) + { + Disconnect(); + var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), TextManager.Get("ServerProcessClosed")); + msgBox.Buttons[0].OnClicked += ReturnToPreviousMenu; + } } } @@ -747,7 +756,7 @@ namespace Barotrauma.Networking { saveFiles.Add(inc.ReadString()); } - GameMain.NetLobbyScreen.CampaignSetupUI = MultiPlayerCampaign.StartCampaignSetup(serverSubmarines, saveFiles); + MultiPlayerCampaign.StartCampaignSetup(saveFiles); break; case ServerPacketHeader.PERMISSIONS: ReadPermissions(inc); @@ -768,7 +777,12 @@ namespace Barotrauma.Networking SteamAchievementManager.CheatsEnabled = cheatsEnabled; if (cheatsEnabled) { - new GUIMessageBox(TextManager.Get("CheatsEnabledTitle"), TextManager.Get("CheatsEnabledDescription")); + var cheatMessageBox = new GUIMessageBox(TextManager.Get("CheatsEnabledTitle"), TextManager.Get("CheatsEnabledDescription")); + cheatMessageBox.Buttons[0].OnClicked += (btn, userdata) => + { + DebugConsole.TextBox.Select(); + return true; + }; } } break; @@ -778,6 +792,9 @@ namespace Barotrauma.Networking case ServerPacketHeader.TRAITOR_MESSAGE: ReadTraitorMessage(inc); break; + case ServerPacketHeader.MISSION: + GameMain.GameSession.Mission?.ClientRead(inc); + break; } } @@ -958,16 +975,13 @@ namespace Barotrauma.Networking switch(messageType) { case TraitorMessageType.Objective: var isTraitor = !string.IsNullOrEmpty(message); + SpawnAsTraitor = isTraitor; + TraitorFirstObjective = message; if (Character != null) { Character.IsTraitor = isTraitor; Character.TraitorCurrentObjective = message; } - else - { - SpawnAsTraitor = isTraitor; - TraitorFirstObjective = message; - } break; case TraitorMessageType.Console: GameMain.Client.AddChatMessage(ChatMessage.Create("", message, ChatMessageType.Console, null)); @@ -1153,6 +1167,21 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.SelectedSub.MD5Hash?.Hash != subHash) { string errorMsg = "Failed to select submarine \"" + subName + "\" (hash: " + subHash + ")."; + if (GameMain.NetLobbyScreen.SelectedSub == null) + { + errorMsg += "\n" + "SelectedSub is null"; + } + else + { + if (GameMain.NetLobbyScreen.SelectedSub.Name != subName) + { + errorMsg += "\n" + "Name mismatch: " + GameMain.NetLobbyScreen.SelectedSub.Name + " != " + subName; + } + if (GameMain.NetLobbyScreen.SelectedSub.MD5Hash?.Hash != subHash) + { + errorMsg += "\n" + "Hash mismatch: " + GameMain.NetLobbyScreen.SelectedSub.MD5Hash?.Hash + " != " + subHash; + } + } DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:FailedToSelectSub" + subName, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); CoroutineManager.StartCoroutine(EndGame("")); @@ -1316,6 +1345,7 @@ namespace Barotrauma.Networking UInt64 steamId = inc.ReadUInt64(); UInt16 nameId = inc.ReadUInt16(); string name = inc.ReadString(); + string preferredJob = inc.ReadString(); UInt16 characterID = inc.ReadUInt16(); bool muted = inc.ReadBoolean(); bool allowKicking = inc.ReadBoolean(); @@ -1327,6 +1357,7 @@ namespace Barotrauma.Networking NameID = nameId, SteamID = steamId, Name = name, + PreferredJob = preferredJob, CharacterID = characterID, Muted = muted, AllowKicking = allowKicking @@ -1353,9 +1384,11 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.AddPlayer(existingClient); } existingClient.NameID = tc.NameID; + existingClient.PreferredJob = tc.PreferredJob; existingClient.Character = null; existingClient.Muted = tc.Muted; existingClient.AllowKicking = tc.AllowKicking; + GameMain.NetLobbyScreen.SetPlayerNameAndJobPreference(existingClient); if (tc.CharacterID > 0) { existingClient.Character = Entity.FindEntityByID(tc.CharacterID) as Character; @@ -1443,7 +1476,7 @@ namespace Barotrauma.Networking bool allowSpectating = inc.ReadBoolean(); YesNoMaybe traitorsEnabled = (YesNoMaybe)inc.ReadRangedInteger(0, 2); - int missionTypeIndex = inc.ReadRangedInteger(0, Enum.GetValues(typeof(MissionType)).Length - 1); + MissionType missionType = (MissionType)inc.ReadRangedInteger(0, (int)MissionType.All); int modeIndex = inc.ReadByte(); string levelSeed = inc.ReadString(); @@ -1461,7 +1494,11 @@ namespace Barotrauma.Networking ReadWriteMessage settingsBuf = new ReadWriteMessage(); settingsBuf.Write(settingsData, 0, settingsLen); settingsBuf.BitPosition = 0; serverSettings.ClientRead(settingsBuf); - + if (!IsServerOwner) + { + ServerInfo info = GameMain.ServerListScreen.UpdateServerInfoWithServerSettings(serverEndpoint, serverSettings); + GameMain.ServerListScreen.AddToRecentServers(info); + } GameMain.NetLobbyScreen.LastUpdateID = updateID; @@ -1475,7 +1512,7 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.TrySelectSub(selectShuttleName, selectShuttleHash, GameMain.NetLobbyScreen.ShuttleList.ListBox); GameMain.NetLobbyScreen.SetTraitorsEnabled(traitorsEnabled); - GameMain.NetLobbyScreen.SetMissionType(missionTypeIndex); + GameMain.NetLobbyScreen.SetMissionType(missionType); if (!allowModeVoting) GameMain.NetLobbyScreen.SelectMode(modeIndex); @@ -1649,6 +1686,15 @@ namespace Barotrauma.Networking outmsg.Write(LastClientListUpdateID); outmsg.Write(nameId); outmsg.Write(name); + var jobPreferences = GameMain.NetLobbyScreen.JobPreferences; + if (jobPreferences.Count > 0) + { + outmsg.Write(jobPreferences[0].First.Identifier); + } + else + { + outmsg.Write(""); + } var campaign = GameMain.GameSession?.GameMode as MultiPlayerCampaign; if (campaign == null || campaign.LastSaveID == 0) @@ -1722,7 +1768,7 @@ namespace Barotrauma.Networking public void SendChatMessage(ChatMessage msg) { - if (clientPeer.ServerConnection == null) return; + if (clientPeer?.ServerConnection == null) { return; } lastQueueChatMsgID++; msg.NetStateID = lastQueueChatMsgID; chatMsgQueue.Add(msg); @@ -1730,7 +1776,7 @@ namespace Barotrauma.Networking public void SendChatMessage(string message, ChatMessageType type = ChatMessageType.Default) { - if (clientPeer.ServerConnection == null) return; + if (clientPeer?.ServerConnection == null) { return; } ChatMessage chatMessage = ChatMessage.Create( gameStarted && myCharacter != null ? myCharacter.Name : name, @@ -1809,19 +1855,6 @@ namespace Barotrauma.Networking subElement.GetChild().TextColor = new Color(subElement.GetChild().TextColor, 1.0f); subElement.UserData = newSub; subElement.ToolTip = newSub.Description; - - GUIButton infoButton = subElement.GetChild(); - if (infoButton == null) - { - int buttonSize = (int)(subElement.Rect.Height * 0.8f); - infoButton = new GUIButton(new RectTransform(new Point(buttonSize), subElement.RectTransform, Anchor.CenterLeft) { AbsoluteOffset = new Point((int)(buttonSize * 0.2f), 0) }, "?"); - } - infoButton.UserData = newSub; - infoButton.OnClicked = (component, userdata) => - { - ((Submarine)userdata).CreatePreviewWindow(new GUIMessageBox("", "", new Vector2(0.25f, 0.25f), new Point(500, 400))); - return true; - }; } if (GameMain.NetLobbyScreen.FailedSelectedSub != null && @@ -1850,9 +1883,12 @@ namespace Barotrauma.Networking string subPath = Path.Combine(SaveUtil.TempPath, gameSessionDoc.Root.GetAttributeString("submarine", "")) + ".sub"; GameMain.GameSession.Submarine = new Submarine(subPath, ""); } - SaveUtil.LoadGame(GameMain.GameSession.SavePath, GameMain.GameSession); GameMain.GameSession?.Submarine?.CheckSubsLeftBehind(); + if (GameMain.GameSession?.Submarine?.Name != null) + { + GameMain.NetLobbyScreen.TryDisplayCampaignSubmarine(GameMain.GameSession.Submarine); + } campaign.LastSaveID = campaign.PendingSaveID; DebugConsole.Log("Campaign save received, save ID " + campaign.LastSaveID); @@ -1940,6 +1976,8 @@ namespace Barotrauma.Networking GameMain.ServerChildProcess = null; } + characterInfo?.Remove(); + VoipClient?.Dispose(); VoipClient = null; GameMain.Client = null; @@ -1963,7 +2001,8 @@ namespace Barotrauma.Networking msg.Write((byte)count); for (int i = 0; i < count; i++) { - msg.Write(jobPreferences[i].Identifier); + msg.Write(jobPreferences[i].First.Identifier); + msg.Write((byte)jobPreferences[i].Second); } } @@ -2137,6 +2176,8 @@ namespace Barotrauma.Networking public void SetupNewCampaign(Submarine sub, string saveName, string mapSeed) { + GameMain.NetLobbyScreen.CampaignSetupFrame.Visible = false; + saveName = Path.GetFileNameWithoutExtension(saveName); IWriteMessage msg = new WriteOnlyMessage(); @@ -2149,12 +2190,12 @@ namespace Barotrauma.Networking msg.Write(sub.MD5Hash.Hash); clientPeer.Send(msg, DeliveryMethod.Reliable); - - GameMain.NetLobbyScreen.CampaignSetupUI = null; } public void SetupLoadCampaign(string saveName) { + GameMain.NetLobbyScreen.CampaignSetupFrame.Visible = false; + IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ClientPacketHeader.CAMPAIGN_SETUP_INFO); @@ -2162,8 +2203,6 @@ namespace Barotrauma.Networking msg.Write(saveName); clientPeer.Send(msg, DeliveryMethod.Reliable); - - GameMain.NetLobbyScreen.CampaignSetupUI = null; } /// @@ -2278,7 +2317,11 @@ namespace Barotrauma.Networking SendChatMessage(message); - textBox.Deselect(); + if (textBox.DeselectAfterMessage) + { + textBox.Deselect(); + } + textBox.Text = ""; return true; @@ -2292,6 +2335,7 @@ namespace Barotrauma.Networking Screen.Selected == GameMain.GameScreen) { inGameHUD.AddToGUIUpdateList(); + GameMain.NetLobbyScreen.FileTransferFrame?.AddToGUIUpdateList(); } } @@ -2305,7 +2349,7 @@ namespace Barotrauma.Networking } else if (Screen.Selected == GameMain.NetLobbyScreen) { - msgBox = GameMain.NetLobbyScreen.TextBox; + msgBox = GameMain.NetLobbyScreen.ChatInput; } if (gameStarted && Screen.Selected == GameMain.GameScreen) @@ -2358,46 +2402,26 @@ namespace Barotrauma.Networking public virtual void Draw(Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch) { if (GUI.DisableHUD || GUI.DisableUpperHUD) return; - + if (fileReceiver != null && fileReceiver.ActiveTransfers.Count > 0) { - Vector2 downloadBarSize = new Vector2(250, 35) * GUI.Scale; - Vector2 pos = new Vector2(GameMain.NetLobbyScreen.InfoFrame.Rect.X, GameMain.GraphicsHeight - downloadBarSize.Y - 5); - - GUI.DrawRectangle(spriteBatch, new Rectangle( - (int)pos.X, - (int)pos.Y, - (int)(fileReceiver.ActiveTransfers.Count * (downloadBarSize.X + 10)), - (int)downloadBarSize.Y), - Color.Black * 0.8f, true); - - for (int i = 0; i < fileReceiver.ActiveTransfers.Count; i++) - { - var transfer = fileReceiver.ActiveTransfers[i]; - - GUI.DrawString(spriteBatch, - pos, - ToolBox.LimitString(TextManager.GetWithVariable("DownloadingFile", "[filename]", transfer.FileName), GUI.SmallFont, (int)downloadBarSize.X), - Color.White, null, 0, GUI.SmallFont); - GUI.DrawProgressBar(spriteBatch, new Vector2(pos.X, -pos.Y - downloadBarSize.Y / 2), new Vector2(downloadBarSize.X * 0.7f, downloadBarSize.Y / 2), transfer.Progress, Color.Green); - GUI.DrawString(spriteBatch, pos + new Vector2(5, downloadBarSize.Y / 2), - MathUtils.GetBytesReadable((long)transfer.Received) + " / " + MathUtils.GetBytesReadable((long)transfer.FileSize), - Color.White, null, 0, GUI.SmallFont); - - if (GUI.DrawButton(spriteBatch, new Rectangle( - (int)(pos.X + downloadBarSize.X * 0.7f), (int)(pos.Y + downloadBarSize.Y / 2), - (int)(downloadBarSize.X * 0.3f), (int)(downloadBarSize.Y / 2)), - TextManager.Get("Cancel"), new Color(0.47f, 0.13f, 0.15f, 0.08f))) - { - CancelFileTransfer(transfer); - fileReceiver.StopTransfer(transfer); - } - - pos.X += (downloadBarSize.X + 10); - } + var transfer = fileReceiver.ActiveTransfers.First(); + GameMain.NetLobbyScreen.FileTransferFrame.Visible = true; + GameMain.NetLobbyScreen.FileTransferTitle.Text = + ToolBox.LimitString( + TextManager.GetWithVariable("DownloadingFile", "[filename]", transfer.FileName), + GameMain.NetLobbyScreen.FileTransferTitle.Font, + GameMain.NetLobbyScreen.FileTransferTitle.Rect.Width); + GameMain.NetLobbyScreen.FileTransferProgressBar.BarSize = transfer.Progress; + GameMain.NetLobbyScreen.FileTransferProgressText.Text = + MathUtils.GetBytesReadable((long)transfer.Received) + " / " + MathUtils.GetBytesReadable((long)transfer.FileSize); } - - if (!gameStarted || Screen.Selected != GameMain.GameScreen) return; + else + { + GameMain.NetLobbyScreen.FileTransferFrame.Visible = false; + } + + if (!gameStarted || Screen.Selected != GameMain.GameScreen) { return; } inGameHUD.DrawManually(spriteBatch); diff --git a/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/LidgrenClientPeer.cs b/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/LidgrenClientPeer.cs index 8835b49e9..a6ea2ced9 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/LidgrenClientPeer.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/LidgrenClientPeer.cs @@ -169,7 +169,7 @@ namespace Barotrauma.Networking outMsg.Write(GameMain.Version.ToString()); - IEnumerable mpContentPackages = GameMain.SelectedPackages.Where(cp => cp.HasMultiplayerIncompatibleContent); + IEnumerable mpContentPackages = GameMain.SelectedPackages.Where(cp => cp.HasMultiplayerIncompatibleContent && !cp.NeedsRestart); outMsg.WriteVariableInt32(mpContentPackages.Count()); foreach (ContentPackage contentPackage in mpContentPackages) { diff --git a/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/SteamP2PClientPeer.cs b/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/SteamP2PClientPeer.cs index ea4376fef..7e6736a07 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/SteamP2PClientPeer.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/Primitives/Peers/SteamP2PClientPeer.cs @@ -202,7 +202,7 @@ namespace Barotrauma.Networking outMsg.Write(GameMain.Version.ToString()); - IEnumerable mpContentPackages = GameMain.SelectedPackages.Where(cp => cp.HasMultiplayerIncompatibleContent); + IEnumerable mpContentPackages = GameMain.SelectedPackages.Where(cp => cp.HasMultiplayerIncompatibleContent && !cp.NeedsRestart); outMsg.WriteVariableUInt32((UInt32)mpContentPackages.Count()); foreach (ContentPackage contentPackage in mpContentPackages) { diff --git a/Barotrauma/BarotraumaClient/Source/Networking/ServerInfo.cs b/Barotrauma/BarotraumaClient/Source/Networking/ServerInfo.cs index 167bb9e89..33beeb8f4 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/ServerInfo.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/ServerInfo.cs @@ -3,6 +3,8 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; +using System.Net; +using System.Xml.Linq; namespace Barotrauma.Networking { @@ -10,8 +12,11 @@ namespace Barotrauma.Networking { public string IP; public string Port; + public string QueryPort; public UInt64 LobbyID; + public UInt64 OwnerID; + public bool OwnerVerified; public string ServerName; public string ServerMessage; @@ -30,12 +35,22 @@ namespace Barotrauma.Networking public SelectionMode? SubSelectionMode; public bool? AllowSpectating; public bool? VoipEnabled; + public bool? KarmaEnabled; + public bool? FriendlyFireEnabled; public bool? AllowRespawn; public YesNoMaybe? TraitorsEnabled; public string GameMode; + public PlayStyle? PlayStyle; + + public bool Recent; + public bool Favorite; public bool? RespondedToSteamQuery = null; + public Facepunch.Steamworks.SteamFriend SteamFriend; + public Facepunch.Steamworks.ISteamMatchmakingPingResponse MatchmakingPingResponse; + public int? PingHQuery; + public string GameVersion; public List ContentPackageNames { @@ -85,59 +100,100 @@ namespace Barotrauma.Networking if (frame == null) return; - var previewContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 1.0f), frame.RectTransform, Anchor.Center)) + var previewContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), frame.RectTransform, Anchor.Center)) { Stretch = true }; - var columnContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), previewContainer.RectTransform)) + var titleContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.035f), previewContainer.RectTransform), true) + { + Color = Color.White * 0.2f + }; + + var title = new GUITextBlock(new RectTransform(new Vector2(0.9f, 0.0f), titleContainer.RectTransform, Anchor.CenterLeft), ServerName, font: GUI.LargeFont) + { + ToolTip = ServerName + }; + title.Text = ToolBox.LimitString(title.Text, title.Font, title.Rect.Width); + + title.Padding = new Vector4(10, 0, 0, 10); + + GUITickBox favoriteTickBox = new GUITickBox(new RectTransform(new Vector2(0.9f, 0.85f), titleContainer.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.BothHeight) { RelativeOffset = new Vector2(0.0f, 0.1f) }, "", null, "GUIServerListFavoriteTickBox") + { + Selected = Favorite, + ToolTip = TextManager.Get(Favorite ? "removefromfavorites" : "addtofavorites"), + OnSelected = (tickbox) => + { + if (tickbox.Selected) + { + GameMain.ServerListScreen.AddToFavoriteServers(this); + } + else + { + GameMain.ServerListScreen.RemoveFromFavoriteServers(this); + } + tickbox.ToolTip = TextManager.Get(tickbox.Selected ? "removefromfavorites" : "addtofavorites"); + return true; + } + }; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), previewContainer.RectTransform), + TextManager.AddPunctuation(':', TextManager.Get("ServerListVersion"), string.IsNullOrEmpty(GameVersion) ? TextManager.Get("Unknown") : GameVersion)); + + PlayStyle playStyle = PlayStyle ?? Networking.PlayStyle.Serious; + + Sprite playStyleBannerSprite = GameMain.ServerListScreen.PlayStyleBanners[(int)playStyle]; + float playStyleBannerAspectRatio = playStyleBannerSprite.SourceRect.Width / (playStyleBannerSprite.SourceRect.Height * 0.65f); + var playStyleBanner = new GUIImage(new RectTransform(new Vector2(1.0f, 1.0f / playStyleBannerAspectRatio), previewContainer.RectTransform, Anchor.TopCenter, scaleBasis: ScaleBasis.BothWidth), + playStyleBannerSprite, null, true); + + var playStyleName = new GUITextBlock(new RectTransform(new Vector2(0.15f, 0.0f), playStyleBanner.RectTransform) { RelativeOffset = new Vector2(0.01f, 0.06f) }, + TextManager.AddPunctuation(':', TextManager.Get("serverplaystyle"), TextManager.Get("servertag."+ playStyle)), textColor: Color.White, + font: GUI.SmallFont, textAlignment: Alignment.Center, + color: GameMain.ServerListScreen.PlayStyleColors[(int)playStyle], style: "GUISlopedHeader"); + playStyleName.RectTransform.NonScaledSize = (playStyleName.Font.MeasureString(playStyleName.Text) + new Vector2(20, 5) * GUI.Scale).ToPoint(); + playStyleName.RectTransform.IsFixedSize = true; + + + var columnContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.45f), previewContainer.RectTransform), isHorizontal: true) + { + Stretch = true + }; + + // Left column ------------------------------------------------------------------------------- + var leftColumnHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.75f, 1.0f), columnContainer.RectTransform), childAnchor: Anchor.Center) + { + Stretch = true + }; + + var leftColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 1.0f), leftColumnHolder.RectTransform)) { Stretch = true }; float elementHeight = 0.075f; - var title = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), columnContainer.RectTransform), ServerName, font: GUI.LargeFont); - title.Text = ToolBox.LimitString(title.Text, title.Font, title.Rect.Width); + // Spacing + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.025f), leftColumn.RectTransform), style: null); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), columnContainer.RectTransform), - TextManager.AddPunctuation(':', TextManager.Get("ServerListVersion"), string.IsNullOrEmpty(GameVersion) ? TextManager.Get("Unknown") : GameVersion)); + var serverMsg = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.3f), leftColumn.RectTransform)) { ScrollBarVisible = true }; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverMsg.Content.RectTransform), ServerMessage, font: GUI.SmallFont, wrap: true) { CanBeFocused = false }; - // left column ----------------------------------------------------------------------------- - - //new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), columnLeft.RectTransform), IP + ":" + Port); - - var serverMsg = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.5f), columnContainer.RectTransform)) { ScrollBarVisible = true }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverMsg.Content.RectTransform), ServerMessage, wrap: true) { CanBeFocused = false }; - - // right column ----------------------------------------------------------------------------- - - /*var playerCount = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), columnRight.RectTransform), TextManager.Get("ServerListPlayers")); - new GUITextBlock(new RectTransform(Vector2.One, playerCount.RectTransform), PlayerCount + "/" + MaxPlayers, textAlignment: Alignment.Right); - - - new GUITickBox(new RectTransform(new Vector2(1, elementHeight), columnRight.RectTransform), "Round running") - { - Selected = GameStarted, - CanBeFocused = false - };*/ - - var gameMode = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), columnContainer.RectTransform), TextManager.Get("GameMode")); + var gameMode = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), leftColumn.RectTransform), TextManager.Get("GameMode")); new GUITextBlock(new RectTransform(Vector2.One, gameMode.RectTransform), TextManager.Get(string.IsNullOrEmpty(GameMode) ? "Unknown" : "GameMode." + GameMode, returnNull: true) ?? GameMode, textAlignment: Alignment.Right); - var traitors = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), columnContainer.RectTransform), TextManager.Get("Traitors")); + /*var traitors = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), bodyContainer.RectTransform), TextManager.Get("Traitors")); + new GUITextBlock(new RectTransform(Vector2.One, traitors.RectTransform), TextManager.Get(!TraitorsEnabled.HasValue ? "Unknown" : TraitorsEnabled.Value.ToString()), textAlignment: Alignment.Right);*/ - new GUITextBlock(new RectTransform(Vector2.One, traitors.RectTransform), TextManager.Get(!TraitorsEnabled.HasValue ? "Unknown" : TraitorsEnabled.Value.ToString()), textAlignment: Alignment.Right); - - var subSelection = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), columnContainer.RectTransform), TextManager.Get("ServerListSubSelection")); + var subSelection = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), leftColumn.RectTransform), TextManager.Get("ServerListSubSelection")); new GUITextBlock(new RectTransform(Vector2.One, subSelection.RectTransform), TextManager.Get(!SubSelectionMode.HasValue ? "Unknown" : SubSelectionMode.Value.ToString()), textAlignment: Alignment.Right); - var modeSelection = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), columnContainer.RectTransform), TextManager.Get("ServerListModeSelection")); + var modeSelection = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), leftColumn.RectTransform), TextManager.Get("ServerListModeSelection")); new GUITextBlock(new RectTransform(Vector2.One, modeSelection.RectTransform), TextManager.Get(!ModeSelectionMode.HasValue ? "Unknown" : ModeSelectionMode.Value.ToString()), textAlignment: Alignment.Right); - var allowSpectating = new GUITickBox(new RectTransform(new Vector2(1, elementHeight), columnContainer.RectTransform), TextManager.Get("ServerListAllowSpectating")) + var allowSpectating = new GUITickBox(new RectTransform(new Vector2(1, elementHeight), leftColumn.RectTransform), TextManager.Get("ServerListAllowSpectating")) { CanBeFocused = false }; @@ -146,7 +202,7 @@ namespace Barotrauma.Networking else allowSpectating.Selected = AllowSpectating.Value; - var allowRespawn = new GUITickBox(new RectTransform(new Vector2(1, elementHeight), columnContainer.RectTransform), TextManager.Get("ServerSettingsAllowRespawning")) + var allowRespawn = new GUITickBox(new RectTransform(new Vector2(1, elementHeight), leftColumn.RectTransform), TextManager.Get("ServerSettingsAllowRespawning")) { CanBeFocused = false }; @@ -155,16 +211,16 @@ namespace Barotrauma.Networking else allowRespawn.Selected = AllowRespawn.Value; - var voipEnabledTickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), columnContainer.RectTransform), TextManager.Get("serversettingsvoicechatenabled")) + /*var voipEnabledTickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), bodyContainer.RectTransform), TextManager.Get("serversettingsvoicechatenabled")) { CanBeFocused = false }; if (!VoipEnabled.HasValue) new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.8f), voipEnabledTickBox.Box.RectTransform, Anchor.Center), "?", textAlignment: Alignment.Center); else - voipEnabledTickBox.Selected = VoipEnabled.Value; + voipEnabledTickBox.Selected = VoipEnabled.Value;*/ - var usingWhiteList = new GUITickBox(new RectTransform(new Vector2(1, elementHeight), columnContainer.RectTransform), TextManager.Get("ServerListUsingWhitelist")) + var usingWhiteList = new GUITickBox(new RectTransform(new Vector2(1, elementHeight), leftColumn.RectTransform), TextManager.Get("ServerListUsingWhitelist")) { CanBeFocused = false }; @@ -174,15 +230,15 @@ namespace Barotrauma.Networking usingWhiteList.Selected = UsingWhiteList.Value; - columnContainer.RectTransform.SizeChanged += () => + leftColumn.RectTransform.SizeChanged += () => { - GUITextBlock.AutoScaleAndNormalize(allowSpectating.TextBlock, allowRespawn.TextBlock, voipEnabledTickBox.TextBlock, usingWhiteList.TextBlock); + GUITextBlock.AutoScaleAndNormalize(allowSpectating.TextBlock, allowRespawn.TextBlock, usingWhiteList.TextBlock); }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), columnContainer.RectTransform), + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), leftColumn.RectTransform), TextManager.Get("ServerListContentPackages")); - var contentPackageList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.3f), columnContainer.RectTransform)) { ScrollBarVisible = true }; + var contentPackageList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.2f), leftColumn.RectTransform)) { ScrollBarVisible = true }; if (ContentPackageNames.Count == 0) { new GUITextBlock(new RectTransform(Vector2.One, contentPackageList.Content.RectTransform), TextManager.Get("Unknown"), textAlignment: Alignment.Center) @@ -231,7 +287,7 @@ namespace Barotrauma.Networking } if (availableWorkshopUrls.Count > 0) { - var workshopBtn = new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), columnContainer.RectTransform), TextManager.Get("ServerListSubscribeMissingPackages")) + var workshopBtn = new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), leftColumn.RectTransform), TextManager.Get("ServerListSubscribeMissingPackages")) { ToolTip = TextManager.Get(SteamManager.IsInitialized ? "ServerListSubscribeMissingPackagesTooltip" : "ServerListSubscribeMissingPackagesTooltipNoSteam"), Enabled = SteamManager.IsInitialized, @@ -246,12 +302,237 @@ namespace Barotrauma.Networking } } + // Spacing + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), leftColumn.RectTransform), style: null); + + // Right column ------------------------------------------------------------------------------ + + var rightColumnBackground = new GUIFrame(new RectTransform(new Vector2(0.2f, 1.0f), columnContainer.RectTransform), style: null) + { + Color = Color.Black * 0.25f + }; + + var rightColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 1.0f), rightColumnBackground.RectTransform, Anchor.Center)); + + // playstyle tags ----------------------------------------------------------------------------- + + var playStyleTags = GetPlayStyleTags(); + foreach (string tag in playStyleTags) + { + if (!GameMain.ServerListScreen.PlayStyleIcons.ContainsKey(tag)) { continue; } + + new GUIImage(new RectTransform(Vector2.One, rightColumn.RectTransform, scaleBasis: ScaleBasis.BothWidth), + GameMain.ServerListScreen.PlayStyleIcons[tag], scaleToFit: true) + { + ToolTip = TextManager.Get("servertagdescription." + tag), + Color = GameMain.ServerListScreen.PlayStyleIconColors[tag] + }; + } + + + /*var playerCount = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), columnRight.RectTransform), TextManager.Get("ServerListPlayers")); + new GUITextBlock(new RectTransform(Vector2.One, playerCount.RectTransform), PlayerCount + "/" + MaxPlayers, textAlignment: Alignment.Right); + + + new GUITickBox(new RectTransform(new Vector2(1, elementHeight), columnRight.RectTransform), "Round running") + { + Selected = GameStarted, + CanBeFocused = false + };*/ + + // ----------------------------------------------------------------------------- - foreach (GUIComponent c in columnContainer.Children) + foreach (GUIComponent c in leftColumn.Children) { if (c is GUITextBlock textBlock) textBlock.Padding = Vector4.Zero; } } + + public IEnumerable GetPlayStyleTags() + { + List tags = new List(); + if (KarmaEnabled.HasValue) + { + tags.Add(KarmaEnabled.Value ? "karma.true" : "karma.false"); + } + if (TraitorsEnabled.HasValue) + { + tags.Add(TraitorsEnabled.Value == YesNoMaybe.Maybe ? + "traitors.maybe" : + (TraitorsEnabled.Value == YesNoMaybe.Yes ? "traitors.true" : "traitors.false")); + } + if (VoipEnabled.HasValue) + { + tags.Add(VoipEnabled.Value ? "voip.true" : "voip.false"); + } + if (FriendlyFireEnabled.HasValue) + { + tags.Add(FriendlyFireEnabled.Value ? "friendlyfire.true" : "friendlyfire.false"); + } + if (ContentPackageNames.Count > 0) + { + tags.Add(ContentPackageNames.Count > 1 || ContentPackageNames[0] != GameMain.VanillaContent?.Name ? "modded.true" : "modded.false"); + } + return tags; + } + + public static ServerInfo FromXElement(XElement element) + { + ServerInfo info = new ServerInfo() + { + ServerName = element.GetAttributeString("ServerName", ""), + ServerMessage = element.GetAttributeString("ServerMessage", ""), + IP = element.GetAttributeString("IP", ""), + Port = element.GetAttributeString("Port", ""), + QueryPort = element.GetAttributeString("QueryPort", ""), + OwnerID = element.GetAttributeSteamID("OwnerID",0) + }; + + info.RespondedToSteamQuery = null; + + info.GameMode = element.GetAttributeString("GameMode", ""); + info.GameVersion = element.GetAttributeString("GameVersion", ""); + info.MaxPlayers = element.GetAttributeInt("MaxPlayers", 0); + + if (Enum.TryParse(element.GetAttributeString("PlayStyle", ""), out PlayStyle playStyleTemp)) { info.PlayStyle = playStyleTemp; } + if (bool.TryParse(element.GetAttributeString("UsingWhiteList", ""), out bool whitelistTemp)) { info.UsingWhiteList = whitelistTemp; } + if (Enum.TryParse(element.GetAttributeString("TraitorsEnabled", ""), out YesNoMaybe traitorsTemp)) { info.TraitorsEnabled = traitorsTemp; } + if (Enum.TryParse(element.GetAttributeString("SubSelectionMode", ""), out SelectionMode subSelectionTemp)) { info.SubSelectionMode = subSelectionTemp; } + if (Enum.TryParse(element.GetAttributeString("ModeSelectionMode", ""), out SelectionMode modeSelectionTemp)) { info.ModeSelectionMode = subSelectionTemp; } + if (bool.TryParse(element.GetAttributeString("VoipEnabled", ""), out bool voipTemp)) { info.VoipEnabled = voipTemp; } + if (bool.TryParse(element.GetAttributeString("KarmaEnabled", ""), out bool karmaTemp)) { info.KarmaEnabled = karmaTemp; } + if (bool.TryParse(element.GetAttributeString("FriendlyFireEnabled", ""), out bool friendlyFireTemp)) { info.FriendlyFireEnabled = friendlyFireTemp; } + + info.HasPassword = element.GetAttributeBool("HasPassword", false); + + return info; + } + + public void QueryLiveInfo(Action onServerRulesReceived) + { + if (!SteamManager.IsInitialized) { return; } + + if (int.TryParse(QueryPort, out _)) + { + if (PingHQuery != null) + { + SteamManager.Instance.ServerList.CancelHQuery(PingHQuery.Value); + PingHQuery = null; + } + + MatchmakingPingResponse = new Facepunch.Steamworks.ISteamMatchmakingPingResponse( + SteamManager.Instance.Client, + (server) => + { + ServerName = server.Name; + RespondedToSteamQuery = true; + PlayerCount = server.Players; + MaxPlayers = server.MaxPlayers; + HasPassword = server.Passworded; + PingChecked = true; + Ping = server.Ping; + LobbyID = 0; + server.FetchRules(); + server.OnReceivedRules += (bool received) => { SteamManager.OnReceivedRules(server, this, received); onServerRulesReceived?.Invoke(this); }; + }, + () => + { + RespondedToSteamQuery = false; + }); + + PingHQuery = SteamManager.Instance.ServerList.HQueryPing(MatchmakingPingResponse, IPAddress.Parse(IP), int.Parse(QueryPort)); + } + else if (OwnerID != 0) + { + if (SteamFriend == null) + { + SteamFriend = SteamManager.Instance.Friends.Get(OwnerID); + } + if (LobbyID == 0) + { + SteamFriend.Refresh(); + if (SteamFriend.IsPlayingThisGame && SteamFriend.ServerLobbyId != 0) + { + LobbyID = SteamFriend.ServerLobbyId; + SteamManager.Instance.LobbyList.SetManualLobbyDataCallback(LobbyID, (lobby) => + { + SteamManager.Instance.LobbyList.SetManualLobbyDataCallback(LobbyID, null); + + if (string.IsNullOrWhiteSpace(lobby.GetData("haspassword"))) { return; } + bool.TryParse(lobby.GetData("haspassword"), out bool hasPassword); + int.TryParse(lobby.GetData("playercount"), out int currPlayers); + int.TryParse(lobby.GetData("maxplayernum"), out int maxPlayers); + //UInt64.TryParse(lobby.GetData("connectsteamid"), out ulong connectSteamId); + string ip = lobby.GetData("hostipaddress"); + UInt64 ownerId = SteamManager.SteamIDStringToUInt64(lobby.GetData("lobbyowner")); + + if (OwnerID != ownerId) { return; } + + if (string.IsNullOrWhiteSpace(ip)) { ip = ""; } + + ServerName = lobby.Name; + Port = ""; + QueryPort = ""; + IP = ip; + PlayerCount = currPlayers; + MaxPlayers = maxPlayers; + HasPassword = hasPassword; + RespondedToSteamQuery = true; + LobbyID = lobby.LobbyID; + OwnerID = ownerId; + PingChecked = false; + OwnerVerified = true; + SteamManager.AssignLobbyDataToServerInfo(lobby, this); + + onServerRulesReceived?.Invoke(this); + }); + SteamManager.Instance.LobbyList.RequestLobbyData(LobbyID); + } + else + { + RespondedToSteamQuery = false; + } + } + } + } + + public XElement ToXElement() + { + if (OwnerID == 0 && string.IsNullOrEmpty(Port)) + { + return null; //can't save this one since it's not set up correctly + } + + XElement element = new XElement("ServerInfo"); + + element.SetAttributeValue("ServerName", ServerName); + element.SetAttributeValue("ServerMessage", ServerMessage); + if (OwnerID == 0) + { + element.SetAttributeValue("IP", IP); + element.SetAttributeValue("Port", Port); + element.SetAttributeValue("QueryPort", QueryPort); + } + else + { + element.SetAttributeValue("OwnerID", SteamManager.SteamIDUInt64ToString(OwnerID)); + } + + element.SetAttributeValue("GameMode", GameMode ?? ""); + element.SetAttributeValue("GameVersion", GameVersion ?? ""); + element.SetAttributeValue("MaxPlayers", MaxPlayers); + if (PlayStyle.HasValue) { element.SetAttributeValue("PlayStyle", PlayStyle.Value.ToString()); } + if (UsingWhiteList.HasValue) { element.SetAttributeValue("UsingWhiteList", UsingWhiteList.Value.ToString()); } + if (TraitorsEnabled.HasValue) { element.SetAttributeValue("TraitorsEnabled", TraitorsEnabled.Value.ToString()); } + if (SubSelectionMode.HasValue) { element.SetAttributeValue("SubSelectionMode", SubSelectionMode.Value.ToString()); } + if (ModeSelectionMode.HasValue) { element.SetAttributeValue("ModeSelectionMode", ModeSelectionMode.Value.ToString()); } + if (VoipEnabled.HasValue) { element.SetAttributeValue("VoipEnabled", VoipEnabled.Value.ToString()); } + if (KarmaEnabled.HasValue) { element.SetAttributeValue("KarmaEnabled", KarmaEnabled.Value.ToString()); } + if (FriendlyFireEnabled.HasValue) { element.SetAttributeValue("FriendlyFireEnabled", FriendlyFireEnabled.Value.ToString()); } + element.SetAttributeValue("HasPassword", HasPassword.ToString()); + + return element; + } } } diff --git a/Barotrauma/BarotraumaClient/Source/Networking/ServerLog.cs b/Barotrauma/BarotraumaClient/Source/Networking/ServerLog.cs index da33bf8b0..dc9601c26 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/ServerLog.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/ServerLog.cs @@ -67,7 +67,10 @@ namespace Barotrauma.Networking y += 20; } - GUITextBlock.AutoScaleAndNormalize(tickBoxes.Select(t => t.TextBlock)); + tickBoxes.Last().TextBlock.RectTransform.SizeChanged += () => + { + GUITextBlock.AutoScaleAndNormalize(tickBoxes.Select(t => t.TextBlock)); + }; var currLines = lines.ToList(); @@ -81,26 +84,76 @@ namespace Barotrauma.Networking if (listBox.BarScroll == 0.0f || listBox.BarScroll == 1.0f) listBox.BarScroll = 1.0f; - GUIButton closeButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.05f), innerFrame.RectTransform, Anchor.BottomRight) { RelativeOffset = new Vector2(0.02f, 0.03f) }, TextManager.Get("Close")); - closeButton.OnClicked = (button, userData) => + GUIButton closeButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.05f), innerFrame.RectTransform, Anchor.BottomRight) { RelativeOffset = new Vector2(0.02f, 0.03f) }, TextManager.Get("Close")) { - LogFrame = null; - return true; + OnClicked = (button, userData) => + { + LogFrame = null; + return true; + } }; msgFilter = ""; } + public void AssignLogFrame(GUIListBox inListBox, GUIComponent tickBoxContainer, GUITextBox searchBox) + { + searchBox.OnTextChanged += (textBox, text) => + { + msgFilter = text; + FilterMessages(); + return true; + }; + + tickBoxContainer.ClearChildren(); + + List tickBoxes = new List(); + foreach (MessageType msgType in Enum.GetValues(typeof(MessageType))) + { + var tickBox = new GUITickBox(new RectTransform(new Point(tickBoxContainer.Rect.Width, 16), tickBoxContainer.RectTransform), TextManager.Get("ServerLog." + messageTypeName[(int)msgType]), font: GUI.SmallFont) + { + Selected = true, + TextColor = messageColor[(int)msgType], + OnSelected = (GUITickBox tb) => + { + msgTypeHidden[(int)msgType] = !tb.Selected; + FilterMessages(); + return true; + } + }; + tickBox.Selected = !msgTypeHidden[(int)msgType]; + tickBoxes.Add(tickBox); + } + tickBoxes.Last().TextBlock.RectTransform.SizeChanged += () => + { + GUITextBlock.AutoScaleAndNormalize(tickBoxes.Select(t => t.TextBlock)); + }; + + inListBox.ClearChildren(); + listBox = inListBox; + + var currLines = lines.ToList(); + foreach (LogMessage line in currLines) + { + AddLine(line); + } + FilterMessages(); + + listBox.UpdateScrollBarSize(); + } + private void AddLine(LogMessage line) { float prevSize = listBox.BarSize; - var textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), listBox.Content.RectTransform), - line.Text, wrap: true, font: GUI.SmallFont); - textBlock.TextColor = messageColor[(int)line.Type]; - textBlock.Visible = !msgTypeHidden[(int)line.Type]; - textBlock.CanBeFocused = false; - textBlock.UserData = line; + var textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), listBox.Content.RectTransform), + line.Text, wrap: true, font: GUI.SmallFont) + { + TextColor = messageColor[(int)line.Type], + Visible = !msgTypeHidden[(int)line.Type], + CanBeFocused = false, + UserData = line + }; if ((prevSize == 1.0f && listBox.BarScroll == 0.0f) || (prevSize < 1.0f && listBox.BarScroll == 1.0f)) listBox.BarScroll = 1.0f; } diff --git a/Barotrauma/BarotraumaClient/Source/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/Source/Networking/ServerSettings.cs index bf9e4b337..da1301026 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/ServerSettings.cs @@ -52,7 +52,7 @@ namespace Barotrauma.Networking scrollBar.BarScrollValue = (float)value; } } - else if (GUIComponent is GUIRadioButtonGroup radioButtonGroup) radioButtonGroup.Selected = (Enum)value; + else if (GUIComponent is GUIRadioButtonGroup radioButtonGroup) radioButtonGroup.Selected = (int)value; else if (GUIComponent is GUIDropDown dropdown) dropdown.SelectItem(value); else if (GUIComponent is GUINumberInput numInput) { @@ -147,7 +147,7 @@ namespace Barotrauma.Networking } } - public void ClientAdminWrite(NetFlags dataToSend, int missionType = 0, float? levelDifficulty = null, bool? autoRestart = null, int traitorSetting = 0, int botCount = 0, int botSpawnMode = 0, bool? useRespawnShuttle = null) + public void ClientAdminWrite(NetFlags dataToSend, int? missionTypeOr = null, int? missionTypeAnd = null, float? levelDifficulty = null, bool? autoRestart = null, int traitorSetting = 0, int botCount = 0, int botSpawnMode = 0, bool? useRespawnShuttle = null) { if (!GameMain.Client.HasPermission(Networking.ClientPermissions.ManageSettings)) return; @@ -200,7 +200,8 @@ namespace Barotrauma.Networking if (dataToSend.HasFlag(NetFlags.Misc)) { - outMsg.Write((byte)(missionType + 1)); + outMsg.WriteRangedInteger(missionTypeOr ?? (int)Barotrauma.MissionType.None, 0, (int)Barotrauma.MissionType.All); + outMsg.WriteRangedInteger(missionTypeAnd ?? (int)Barotrauma.MissionType.All, 0, (int)Barotrauma.MissionType.All); outMsg.Write((byte)(traitorSetting + 1)); outMsg.Write((byte)(botCount + 1)); outMsg.Write((byte)(botSpawnMode + 1)); @@ -330,7 +331,30 @@ namespace Barotrauma.Networking }; //*********************************************** - + + // Play Style Selection + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), serverTab.RectTransform), TextManager.Get("ServerSettingsPlayStyle")); + var playStyleSelection = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.16f), serverTab.RectTransform)) + { + AutoHideScrollBar = true, + UseGridLayout = true + }; + + List playStyleTickBoxes = new List(); + GUIRadioButtonGroup selectionPlayStyle = new GUIRadioButtonGroup(); + foreach (PlayStyle playStyle in Enum.GetValues(typeof(PlayStyle))) + { + var selectionTick = new GUITickBox(new RectTransform(new Vector2(0.32f, 0.49f), playStyleSelection.Content.RectTransform), TextManager.Get("servertag." + playStyle), font: GUI.SmallFont, style: "GUIRadioButton") + { + ToolTip = TextManager.Get("servertagdescription." + playStyle) + }; + selectionPlayStyle.AddRadioButton((int)playStyle, selectionTick); + playStyleTickBoxes.Add(selectionTick); + } + GetPropertyData("PlayStyle").AssignGUIComponent(selectionPlayStyle); + GUITextBlock.AutoScaleAndNormalize(playStyleTickBoxes.Select(t => t.TextBlock)); + + // Sub Selection new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), serverTab.RectTransform), TextManager.Get("ServerSettingsSubSelection")); var selectionFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), serverTab.RectTransform), isHorizontal: true) { @@ -341,12 +365,13 @@ namespace Barotrauma.Networking GUIRadioButtonGroup selectionMode = new GUIRadioButtonGroup(); for (int i = 0; i < 3; i++) { - var selectionTick = new GUITickBox(new RectTransform(new Vector2(0.3f, 1.0f), selectionFrame.RectTransform), TextManager.Get(((SelectionMode)i).ToString()), font: GUI.SmallFont); - selectionMode.AddRadioButton((SelectionMode)i, selectionTick); + var selectionTick = new GUITickBox(new RectTransform(new Vector2(0.3f, 1.0f), selectionFrame.RectTransform), TextManager.Get(((SelectionMode)i).ToString()), font: GUI.SmallFont, style: "GUIRadioButton"); + selectionMode.AddRadioButton(i, selectionTick); } DebugConsole.NewMessage(SubSelectionMode.ToString(), Color.White); GetPropertyData("SubSelectionMode").AssignGUIComponent(selectionMode); + // Mode Selection new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), serverTab.RectTransform), TextManager.Get("ServerSettingsModeSelection")); selectionFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), serverTab.RectTransform), isHorizontal: true) { @@ -357,8 +382,8 @@ namespace Barotrauma.Networking selectionMode = new GUIRadioButtonGroup(); for (int i = 0; i < 3; i++) { - var selectionTick = new GUITickBox(new RectTransform(new Vector2(0.3f, 1.0f), selectionFrame.RectTransform), TextManager.Get(((SelectionMode)i).ToString()), font: GUI.SmallFont); - selectionMode.AddRadioButton((SelectionMode)i, selectionTick); + var selectionTick = new GUITickBox(new RectTransform(new Vector2(0.3f, 1.0f), selectionFrame.RectTransform), TextManager.Get(((SelectionMode)i).ToString()), font: GUI.SmallFont, style: "GUIRadioButton"); + selectionMode.AddRadioButton(i, selectionTick); } GetPropertyData("ModeSelectionMode").AssignGUIComponent(selectionMode); @@ -715,6 +740,18 @@ namespace Barotrauma.Networking RelativeSpacing = 0.02f }; + var allowFriendlyFire = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), antigriefingTab.RectTransform), + TextManager.Get("ServerSettingsAllowFriendlyFire")); + GetPropertyData("AllowFriendlyFire").AssignGUIComponent(allowFriendlyFire); + + var allowRewiring = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), antigriefingTab.RectTransform), + TextManager.Get("ServerSettingsAllowRewiring")); + GetPropertyData("AllowRewiring").AssignGUIComponent(allowRewiring); + + var allowDisguises = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), antigriefingTab.RectTransform), + TextManager.Get("ServerSettingsAllowDisguises")); + GetPropertyData("AllowDisguises").AssignGUIComponent(allowDisguises); + var voteKickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), antigriefingTab.RectTransform), TextManager.Get("ServerSettingsAllowVoteKick")); GetPropertyData("AllowVoteKick").AssignGUIComponent(voteKickBox); diff --git a/Barotrauma/BarotraumaClient/Source/Networking/SteamManager.cs b/Barotrauma/BarotraumaClient/Source/Networking/SteamManager.cs index ca17ec9db..f34de749d 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/SteamManager.cs @@ -20,6 +20,8 @@ namespace Barotrauma.Steam public Facepunch.Steamworks.Auth Auth => client?.Auth; public Facepunch.Steamworks.Lobby Lobby => client?.Lobby; public Facepunch.Steamworks.LobbyList LobbyList => client?.LobbyList; + public Facepunch.Steamworks.ServerList ServerList => client?.ServerList; + public Facepunch.Steamworks.Client Client => client; private SteamManager() { @@ -75,7 +77,8 @@ namespace Barotrauma.Steam Joining, Joined } - private static UInt64 lobbyID = 0; + + public static UInt64 LobbyID { get; private set; } = 0; private static LobbyState lobbyState = LobbyState.NotConnected; private static string lobbyIP = ""; private static Thread lobbyIPRetrievalThread; @@ -167,7 +170,7 @@ namespace Barotrauma.Steam lobbyIPRetrievalThread.Start(); lobbyState = LobbyState.Owner; - lobbyID = instance.client.Lobby.CurrentLobby; + LobbyID = instance.client.Lobby.CurrentLobby; UpdateLobby(serverSettings); }; if (lobbyState != LobbyState.NotConnected) { return; } @@ -192,16 +195,16 @@ namespace Barotrauma.Steam { return; } - + var contentPackages = GameMain.Config.SelectedContentPackages.Where(cp => cp.HasMultiplayerIncompatibleContent); - + instance.client.Lobby.Name = serverSettings.ServerName; - instance.client.Lobby.Owner = Steam.SteamManager.GetSteamID(); - instance.client.Lobby.MaxMembers = serverSettings.MaxPlayers+10; - instance.client.Lobby.CurrentLobbyData.SetData("playercount", (GameMain.Client?.ConnectedClients?.Count??0).ToString()); + instance.client.Lobby.Owner = GetSteamID(); + instance.client.Lobby.MaxMembers = serverSettings.MaxPlayers + 10; + instance.client.Lobby.CurrentLobbyData.SetData("playercount", (GameMain.Client?.ConnectedClients?.Count ?? 0).ToString()); instance.client.Lobby.CurrentLobbyData.SetData("maxplayernum", serverSettings.MaxPlayers.ToString()); instance.client.Lobby.CurrentLobbyData.SetData("hostipaddress", lobbyIP); - //instance.client.Lobby.CurrentLobbyData.SetData("connectsteamid", Steam.SteamManager.GetSteamID().ToString()); + instance.client.Lobby.CurrentLobbyData.SetData("lobbyowner", SteamIDUInt64ToString(GetSteamID())); instance.client.Lobby.CurrentLobbyData.SetData("haspassword", serverSettings.HasPassword.ToString()); instance.client.Lobby.CurrentLobbyData.SetData("message", serverSettings.ServerMessageText); @@ -216,9 +219,12 @@ namespace Barotrauma.Steam instance.client.Lobby.CurrentLobbyData.SetData("voicechatenabled", serverSettings.VoiceChatEnabled.ToString()); instance.client.Lobby.CurrentLobbyData.SetData("allowspectating", serverSettings.AllowSpectating.ToString()); instance.client.Lobby.CurrentLobbyData.SetData("allowrespawn", serverSettings.AllowRespawn.ToString()); + instance.client.Lobby.CurrentLobbyData.SetData("karmaenabled", serverSettings.KarmaEnabled.ToString()); + instance.client.Lobby.CurrentLobbyData.SetData("friendlyfireenabled", serverSettings.AllowFriendlyFire.ToString()); instance.client.Lobby.CurrentLobbyData.SetData("traitors", serverSettings.TraitorsEnabled.ToString()); instance.client.Lobby.CurrentLobbyData.SetData("gamestarted", GameMain.Client.GameStarted.ToString()); - instance.client.Lobby.CurrentLobbyData.SetData("gamemode", GameMain.NetLobbyScreen?.SelectedMode?.Identifier??""); + instance.client.Lobby.CurrentLobbyData.SetData("playstyle", serverSettings.PlayStyle.ToString()); + instance.client.Lobby.CurrentLobbyData.SetData("gamemode", GameMain.NetLobbyScreen?.SelectedMode?.Identifier ?? ""); DebugConsole.Log("Lobby updated!"); } @@ -232,7 +238,7 @@ namespace Barotrauma.Steam lobbyIPRetrievalThread = null; instance.client.Lobby.Leave(); - lobbyID = 0; + LobbyID = 0; lobbyIP = ""; lobbyState = LobbyState.NotConnected; @@ -242,7 +248,7 @@ namespace Barotrauma.Steam public static void JoinLobby(UInt64 id, bool joinServer) { if (instance.client.Lobby.CurrentLobby == id) { return; } - if (lobbyID == id) { return; } + if (LobbyID == id) { return; } instance.client.Lobby.OnLobbyJoined = (success) => { try @@ -253,7 +259,7 @@ namespace Barotrauma.Steam return; } lobbyState = LobbyState.Joined; - lobbyID = instance.client.Lobby.CurrentLobby; + LobbyID = instance.client.Lobby.CurrentLobby; if (joinServer) { GameMain.Instance.ConnectLobby = 0; @@ -267,7 +273,7 @@ namespace Barotrauma.Steam } }; lobbyState = LobbyState.Joining; - lobbyID = id; + LobbyID = id; instance.client.Lobby.Join(id); } @@ -314,9 +320,11 @@ namespace Barotrauma.Steam query.OnUpdate = () => { UpdateServerQuery(query, onServerFound, onServerRulesReceived, includeUnresponsive: true); }; query.OnFinished = onFinished; +#if !DEBUG var localQuery = instance.client.ServerList.Local(filter); localQuery.OnUpdate = () => { UpdateServerQuery(localQuery, onServerFound, onServerRulesReceived, includeUnresponsive: true); }; localQuery.OnFinished = onFinished; +#endif instance.client.LobbyList.OnLobbiesUpdated = () => { UpdateLobbyQuery(onServerFound, onServerRulesReceived, onFinished); }; instance.client.LobbyList.Refresh(); @@ -382,6 +390,7 @@ namespace Barotrauma.Steam bool.TryParse(lobby.GetData("haspassword"), out bool hasPassword); int.TryParse(lobby.GetData("playercount"), out int currPlayers); int.TryParse(lobby.GetData("maxplayernum"), out int maxPlayers); + UInt64 ownerId = SteamIDStringToUInt64(lobby.GetData("lobbyowner")); //UInt64.TryParse(lobby.GetData("connectsteamid"), out ulong connectSteamId); string ip = lobby.GetData("hostipaddress"); if (string.IsNullOrWhiteSpace(ip)) { ip = ""; } @@ -390,41 +399,17 @@ namespace Barotrauma.Steam { ServerName = lobby.Name, Port = "", + QueryPort = "", IP = ip, PlayerCount = currPlayers, MaxPlayers = maxPlayers, HasPassword = hasPassword, RespondedToSteamQuery = true, - LobbyID = lobby.LobbyID + LobbyID = lobby.LobbyID, + OwnerID = ownerId }; serverInfo.PingChecked = false; - serverInfo.ServerMessage = lobby.GetData("message"); - serverInfo.GameVersion = lobby.GetData("version"); - - serverInfo.ContentPackageNames.AddRange(lobby.GetData("contentpackage").Split(',')); - serverInfo.ContentPackageHashes.AddRange(lobby.GetData("contentpackagehash").Split(',')); - serverInfo.ContentPackageWorkshopUrls.AddRange(lobby.GetData("contentpackageurl").Split(',')); - - serverInfo.UsingWhiteList = lobby.GetData("usingwhitelist") == "True"; - SelectionMode selectionMode; - if (Enum.TryParse(lobby.GetData("modeselectionmode"), out selectionMode)) serverInfo.ModeSelectionMode = selectionMode; - if (Enum.TryParse(lobby.GetData("subselectionmode"), out selectionMode)) serverInfo.SubSelectionMode = selectionMode; - - serverInfo.AllowSpectating = lobby.GetData("allowspectating") == "True"; - serverInfo.AllowRespawn = lobby.GetData("allowrespawn") == "True"; - serverInfo.VoipEnabled = lobby.GetData("voicechatenabled") == "True"; - if (Enum.TryParse(lobby.GetData("traitors"), out YesNoMaybe traitorsEnabled)) serverInfo.TraitorsEnabled = traitorsEnabled; - - serverInfo.GameStarted = lobby.GetData("gamestarted") == "True"; - serverInfo.GameMode = lobby.GetData("gamemode"); - - if (serverInfo.ContentPackageNames.Count != serverInfo.ContentPackageHashes.Count || - serverInfo.ContentPackageHashes.Count != serverInfo.ContentPackageWorkshopUrls.Count) - { - //invalid contentpackage info - serverInfo.ContentPackageNames.Clear(); - serverInfo.ContentPackageHashes.Clear(); - } + AssignLobbyDataToServerInfo(lobby, serverInfo); onServerFound(serverInfo); //onServerRulesReceived(serverInfo); @@ -433,6 +418,47 @@ namespace Barotrauma.Steam onFinished(); } + public static void AssignLobbyDataToServerInfo(LobbyList.Lobby lobby, ServerInfo serverInfo) + { + serverInfo.ServerMessage = lobby.GetData("message"); + serverInfo.GameVersion = lobby.GetData("version"); + + serverInfo.ContentPackageNames.AddRange(lobby.GetData("contentpackage").Split(',')); + serverInfo.ContentPackageHashes.AddRange(lobby.GetData("contentpackagehash").Split(',')); + serverInfo.ContentPackageWorkshopUrls.AddRange(lobby.GetData("contentpackageurl").Split(',')); + + serverInfo.UsingWhiteList = getLobbyBool("usingwhitelist"); + SelectionMode selectionMode; + if (Enum.TryParse(lobby.GetData("modeselectionmode"), out selectionMode)) { serverInfo.ModeSelectionMode = selectionMode; } + if (Enum.TryParse(lobby.GetData("subselectionmode"), out selectionMode)) { serverInfo.SubSelectionMode = selectionMode; } + + serverInfo.AllowSpectating = getLobbyBool("allowspectating"); + serverInfo.AllowRespawn = getLobbyBool("allowrespawn"); + serverInfo.VoipEnabled = getLobbyBool("voicechatenabled"); + serverInfo.KarmaEnabled = getLobbyBool("karmaenabled"); + serverInfo.FriendlyFireEnabled = getLobbyBool("friendlyfireenabled"); + if (Enum.TryParse(lobby.GetData("traitors"), out YesNoMaybe traitorsEnabled)) { serverInfo.TraitorsEnabled = traitorsEnabled; } + + serverInfo.GameStarted = lobby.GetData("gamestarted") == "True"; + serverInfo.GameMode = lobby.GetData("gamemode"); + if (Enum.TryParse(lobby.GetData("playstyle"), out PlayStyle playStyle)) serverInfo.PlayStyle = playStyle; + + if (serverInfo.ContentPackageNames.Count != serverInfo.ContentPackageHashes.Count || + serverInfo.ContentPackageHashes.Count != serverInfo.ContentPackageWorkshopUrls.Count) + { + //invalid contentpackage info + serverInfo.ContentPackageNames.Clear(); + serverInfo.ContentPackageHashes.Clear(); + } + + bool? getLobbyBool(string key) + { + string data = lobby.GetData(key); + if (string.IsNullOrEmpty(data)) { return null; } + return data == "True" || data == "true"; + } + } + private static void UpdateServerQuery(ServerList.Request query, Action onServerFound, Action onServerRulesReceived, bool includeUnresponsive) { IEnumerable servers = includeUnresponsive ? @@ -459,6 +485,7 @@ namespace Barotrauma.Steam { ServerName = s.Name, Port = s.ConnectionPort.ToString(), + QueryPort = s.QueryPort.ToString(), IP = s.Address.ToString(), PlayerCount = s.Players, MaxPlayers = s.MaxPlayers, @@ -472,60 +499,72 @@ namespace Barotrauma.Steam { s.FetchRules(); } - s.OnReceivedRules += (bool rulesReceived) => - { - if (!rulesReceived || s.Rules == null) { return; } - - if (s.Rules.ContainsKey("message")) serverInfo.ServerMessage = s.Rules["message"]; - if (s.Rules.ContainsKey("version")) serverInfo.GameVersion = s.Rules["version"]; - - if (s.Rules.ContainsKey("playercount")) - { - if (int.TryParse(s.Rules["playercount"], out int playerCount)) serverInfo.PlayerCount = playerCount; - } - - if (s.Rules.ContainsKey("contentpackage")) serverInfo.ContentPackageNames.AddRange(s.Rules["contentpackage"].Split(',')); - if (s.Rules.ContainsKey("contentpackagehash")) serverInfo.ContentPackageHashes.AddRange(s.Rules["contentpackagehash"].Split(',')); - if (s.Rules.ContainsKey("contentpackageurl")) serverInfo.ContentPackageWorkshopUrls.AddRange(s.Rules["contentpackageurl"].Split(',')); - - if (s.Rules.ContainsKey("usingwhitelist")) serverInfo.UsingWhiteList = s.Rules["usingwhitelist"] == "True"; - if (s.Rules.ContainsKey("modeselectionmode")) - { - if (Enum.TryParse(s.Rules["modeselectionmode"], out SelectionMode selectionMode)) serverInfo.ModeSelectionMode = selectionMode; - } - if (s.Rules.ContainsKey("subselectionmode")) - { - if (Enum.TryParse(s.Rules["subselectionmode"], out SelectionMode selectionMode)) serverInfo.SubSelectionMode = selectionMode; - } - if (s.Rules.ContainsKey("allowspectating")) serverInfo.AllowSpectating = s.Rules["allowspectating"] == "True"; - if (s.Rules.ContainsKey("allowrespawn")) serverInfo.AllowRespawn = s.Rules["allowrespawn"] == "True"; - if (s.Rules.ContainsKey("voicechatenabled")) serverInfo.VoipEnabled = s.Rules["voicechatenabled"] == "True"; - if (s.Rules.ContainsKey("traitors")) - { - if (Enum.TryParse(s.Rules["traitors"], out YesNoMaybe traitorsEnabled)) serverInfo.TraitorsEnabled = traitorsEnabled; - } - - if (s.Rules.ContainsKey("gamestarted")) serverInfo.GameStarted = s.Rules["gamestarted"] == "True"; - - if (s.Rules.ContainsKey("gamemode")) - { - serverInfo.GameMode = s.Rules["gamemode"]; - } - if (serverInfo.ContentPackageNames.Count != serverInfo.ContentPackageHashes.Count || - serverInfo.ContentPackageHashes.Count != serverInfo.ContentPackageWorkshopUrls.Count) - { - //invalid contentpackage info - serverInfo.ContentPackageNames.Clear(); - serverInfo.ContentPackageHashes.Clear(); - } - onServerRulesReceived(serverInfo); - }; + s.OnReceivedRules += (bool received) => { OnReceivedRules(s, serverInfo, received); onServerRulesReceived(serverInfo); }; onServerFound(serverInfo); } query.Responded.Clear(); } + public static void OnReceivedRules(ServerList.Server s, ServerInfo serverInfo, bool rulesReceived) + { + if (!rulesReceived || s.Rules == null) { return; } + + if (s.Rules.ContainsKey("message")) serverInfo.ServerMessage = s.Rules["message"]; + if (s.Rules.ContainsKey("version")) serverInfo.GameVersion = s.Rules["version"]; + + if (s.Rules.ContainsKey("playercount")) + { + if (int.TryParse(s.Rules["playercount"], out int playerCount)) serverInfo.PlayerCount = playerCount; + } + + serverInfo.ContentPackageNames.Clear(); + serverInfo.ContentPackageHashes.Clear(); + serverInfo.ContentPackageWorkshopUrls.Clear(); + if (s.Rules.ContainsKey("contentpackage")) serverInfo.ContentPackageNames.AddRange(s.Rules["contentpackage"].Split(',')); + if (s.Rules.ContainsKey("contentpackagehash")) serverInfo.ContentPackageHashes.AddRange(s.Rules["contentpackagehash"].Split(',')); + if (s.Rules.ContainsKey("contentpackageurl")) serverInfo.ContentPackageWorkshopUrls.AddRange(s.Rules["contentpackageurl"].Split(',')); + + if (s.Rules.ContainsKey("usingwhitelist")) serverInfo.UsingWhiteList = s.Rules["usingwhitelist"] == "True"; + if (s.Rules.ContainsKey("modeselectionmode")) + { + if (Enum.TryParse(s.Rules["modeselectionmode"], out SelectionMode selectionMode)) serverInfo.ModeSelectionMode = selectionMode; + } + if (s.Rules.ContainsKey("subselectionmode")) + { + if (Enum.TryParse(s.Rules["subselectionmode"], out SelectionMode selectionMode)) serverInfo.SubSelectionMode = selectionMode; + } + if (s.Rules.ContainsKey("allowspectating")) serverInfo.AllowSpectating = s.Rules["allowspectating"] == "True"; + if (s.Rules.ContainsKey("allowrespawn")) serverInfo.AllowRespawn = s.Rules["allowrespawn"] == "True"; + if (s.Rules.ContainsKey("voicechatenabled")) serverInfo.VoipEnabled = s.Rules["voicechatenabled"] == "True"; + if (s.Rules.ContainsKey("karmaenabled")) serverInfo.KarmaEnabled = s.Rules["karmaenabled"] == "True"; + if (s.Rules.ContainsKey("friendlyfireenabled")) serverInfo.FriendlyFireEnabled = s.Rules["friendlyfireenabled"] == "True"; + if (s.Rules.ContainsKey("traitors")) + { + if (Enum.TryParse(s.Rules["traitors"], out YesNoMaybe traitorsEnabled)) serverInfo.TraitorsEnabled = traitorsEnabled; + } + + if (s.Rules.ContainsKey("gamestarted")) serverInfo.GameStarted = s.Rules["gamestarted"] == "True"; + + if (s.Rules.ContainsKey("gamemode")) + { + serverInfo.GameMode = s.Rules["gamemode"]; + } + + if (s.Rules.ContainsKey("playstyle")) + { + if (Enum.TryParse(s.Rules["playstyle"], out PlayStyle playStyle)) serverInfo.PlayStyle = playStyle; + } + + if (serverInfo.ContentPackageNames.Count != serverInfo.ContentPackageHashes.Count || + serverInfo.ContentPackageHashes.Count != serverInfo.ContentPackageWorkshopUrls.Count) + { + //invalid contentpackage info + serverInfo.ContentPackageNames.Clear(); + serverInfo.ContentPackageHashes.Clear(); + } + } + private static bool ValidateServerInfo(ServerList.Server server) { if (string.IsNullOrWhiteSpace(server.Name)) { return false; } diff --git a/Barotrauma/BarotraumaClient/Source/Networking/Voip/VoipCapture.cs b/Barotrauma/BarotraumaClient/Source/Networking/Voip/VoipCapture.cs index a2846cd78..bf506cd08 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/Voip/VoipCapture.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/Voip/VoipCapture.cs @@ -26,7 +26,13 @@ namespace Barotrauma.Networking get; private set; } - + + public double LastAmplitude + { + get; + private set; + } + public float Gain { get { return GameMain.Config?.MicrophoneVolume ?? 1.0f; } @@ -191,6 +197,7 @@ namespace Barotrauma.Networking double dB = Math.Min(20 * Math.Log10(maxAmplitude), 0.0); LastdB = dB; + LastAmplitude = maxAmplitude; bool allowEnqueue = false; if (GameMain.WindowActive) diff --git a/Barotrauma/BarotraumaClient/Source/Networking/Voting.cs b/Barotrauma/BarotraumaClient/Source/Networking/Voting.cs index 3be8cc760..9a4b5b7f9 100644 --- a/Barotrauma/BarotraumaClient/Source/Networking/Voting.cs +++ b/Barotrauma/BarotraumaClient/Source/Networking/Voting.cs @@ -16,7 +16,7 @@ namespace Barotrauma allowSubVoting = value; GameMain.NetLobbyScreen.SubList.Enabled = value || (GameMain.Client != null && GameMain.Client.HasPermission(ClientPermissions.SelectSub)); - GameMain.NetLobbyScreen.InfoFrame.FindChild("subvotes", true).Visible = value; + GameMain.NetLobbyScreen.Frame.FindChild("subvotes", true).Visible = value; UpdateVoteTexts(null, VoteType.Sub); GameMain.NetLobbyScreen.SubList.Deselect(); @@ -33,7 +33,7 @@ namespace Barotrauma value || (GameMain.Client != null && GameMain.Client.HasPermission(ClientPermissions.SelectMode)); - GameMain.NetLobbyScreen.InfoFrame.FindChild("modevotes", true).Visible = value; + GameMain.NetLobbyScreen.Frame.FindChild("modevotes", true).Visible = value; //gray out modes that can't be voted foreach (GUITextBlock comp in GameMain.NetLobbyScreen.ModeList.Content.Children) diff --git a/Barotrauma/BarotraumaClient/Source/Particles/ParticleEmitter.cs b/Barotrauma/BarotraumaClient/Source/Particles/ParticleEmitter.cs index 34dfcf6b8..9a513ca6f 100644 --- a/Barotrauma/BarotraumaClient/Source/Particles/ParticleEmitter.cs +++ b/Barotrauma/BarotraumaClient/Source/Particles/ParticleEmitter.cs @@ -25,7 +25,7 @@ namespace Barotrauma.Particles public void Emit(float deltaTime, Vector2 position, Hull hullGuess = null, float angle = 0.0f, float particleRotation = 0.0f, float velocityMultiplier = 1.0f, float sizeMultiplier = 1.0f, float amountMultiplier = 1.0f) { emitTimer += deltaTime * amountMultiplier; - burstEmitTimer += deltaTime; + burstEmitTimer -= deltaTime; if (Prefab.ParticlesPerSecond > 0) { @@ -37,9 +37,9 @@ namespace Barotrauma.Particles } } - if (burstEmitTimer < Prefab.EmitInterval) return; - burstEmitTimer = 0.0f; - + if (burstEmitTimer > 0.0f) { return; } + + burstEmitTimer = Prefab.EmitInterval; for (int i = 0; i < Prefab.ParticleAmount * amountMultiplier; i++) { Emit(position, hullGuess, angle, particleRotation, velocityMultiplier, sizeMultiplier); diff --git a/Barotrauma/BarotraumaClient/Source/Program.cs b/Barotrauma/BarotraumaClient/Source/Program.cs index a38405b09..69a08b8ac 100644 --- a/Barotrauma/BarotraumaClient/Source/Program.cs +++ b/Barotrauma/BarotraumaClient/Source/Program.cs @@ -31,10 +31,12 @@ namespace Barotrauma static void Main(string[] args) { GameMain game = null; + string executableDir = ""; #if !DEBUG try { #endif + executableDir = Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location); SteamManager.Initialize(); game = new GameMain(args); game.Run(); @@ -45,7 +47,7 @@ namespace Barotrauma { try { - CrashDump(game, "crashreport.log", e); + CrashDump(game, Path.Combine(executableDir,"crashreport.log"), e); } catch (Exception e2) { diff --git a/Barotrauma/BarotraumaClient/Source/Screens/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/Source/Screens/CampaignSetupUI.cs index bc4b5ef3a..3de3a82e3 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/CampaignSetupUI.cs @@ -62,25 +62,35 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.02f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("MapSeed")); seedBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, ToolBox.RandomSeed(8)); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.02f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("SelectedSub")); - var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform), isHorizontal: true) + if (!isMultiplayer) { - Stretch = true - }; - subList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.65f), leftColumn.RectTransform)) { ScrollBarVisible = true }; - - var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUI.Font); - var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform, Anchor.CenterRight), font: GUI.Font); - searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; - searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.02f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("SelectedSub")); - searchBox.OnTextChanged += (textBox, text) => { FilterSubs(subList, text); return true; }; - var clearButton = new GUIButton(new RectTransform(new Vector2(0.075f, 1.0f), filterContainer.RectTransform), "x") + var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform), isHorizontal: true) + { + Stretch = true + }; + + subList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.65f), leftColumn.RectTransform)) { ScrollBarVisible = true }; + + var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUI.Font); + var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform, Anchor.CenterRight), font: GUI.Font); + searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; + searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; + + searchBox.OnTextChanged += (textBox, text) => { FilterSubs(subList, text); return true; }; + var clearButton = new GUIButton(new RectTransform(new Vector2(0.075f, 1.0f), filterContainer.RectTransform), "x") + { + OnClicked = (btn, userdata) => { searchBox.Text = ""; FilterSubs(subList, ""); searchBox.Flash(Color.White); return true; } + }; + + subList.OnSelected = OnSubSelected; + } + else // Spacing to fix the multiplayer campaign setup layout { - OnClicked = (btn, userdata) => { searchBox.Text = ""; FilterSubs(subList, ""); searchBox.Flash(Color.White); return true; } - }; - - if (!isMultiplayer) { subList.OnSelected = OnSubSelected; } + //spacing + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.25f), leftColumn.RectTransform), style: null); + } // New game right side subPreviewContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.8f), rightColumn.RectTransform)) @@ -103,7 +113,18 @@ namespace Barotrauma return false; } - if (!(subList.SelectedData is Submarine selectedSub)) { return false; } + Submarine selectedSub = null; + + if (!isMultiplayer) + { + if (!(subList.SelectedData is Submarine)) { return false; } + selectedSub = subList.SelectedData as Submarine; + } + else + { + if (GameMain.NetLobbyScreen.SelectedSub == null) { return false; } + selectedSub = GameMain.NetLobbyScreen.SelectedSub; + } if (string.IsNullOrEmpty(selectedSub.MD5Hash.Hash)) { @@ -189,7 +210,7 @@ namespace Barotrauma leftColumn.Recalculate(); rightColumn.Recalculate(); - UpdateSubList(submarines); + if (submarines != null) { UpdateSubList(submarines); } UpdateLoadMenu(saveFiles); } @@ -228,7 +249,8 @@ namespace Barotrauma msgBox.Buttons[0].OnClicked = (btn, userdata) => { - GameMain.NetLobbyScreen.SelectMode(0); + GameMain.NetLobbyScreen.HighlightMode(GameMain.NetLobbyScreen.SelectedModeIndex); + GameMain.NetLobbyScreen.SelectMode(GameMain.NetLobbyScreen.SelectedModeIndex); CoroutineManager.StopCoroutines("WaitForCampaignSetup"); return true; }; diff --git a/Barotrauma/BarotraumaClient/Source/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/Source/Screens/CampaignUI.cs index 12db6795b..e9da97687 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/CampaignUI.cs @@ -12,28 +12,33 @@ namespace Barotrauma { public enum Tab { Map, Crew, Store, Repair } private Tab selectedTab; - private GUIFrame[] tabs; - private GUIFrame topPanel; + private readonly GUIFrame[] tabs; + private readonly GUIFrame topPanel; - private GUIListBox characterList; + private readonly GUIListBox characterList; private MapEntityCategory selectedItemCategory = MapEntityCategory.Equipment; - private GUIListBox myItemList; - private GUIListBox storeItemList; - private GUITextBox searchBox; + private readonly GUIListBox myItemList; + private readonly GUIListBox storeItemList; + private readonly GUITextBox searchBox; - private GUIComponent missionPanel; - private GUIComponent selectedLocationInfo; - private GUIListBox selectedMissionInfo; + private readonly GUIComponent missionPanel; + private readonly GUIComponent selectedLocationInfo; + private readonly GUIListBox selectedMissionInfo; - private GUIButton repairHullsButton, replaceShuttlesButton, repairItemsButton; + private readonly GUIButton repairHullsButton, replaceShuttlesButton, repairItemsButton; private GUIFrame characterPreviewFrame; - private List tabButtons = new List(); - private List itemCategoryButtons = new List(); - private List missionTickBoxes = new List(); + private bool displayMissionPanelInMapTab; + + private readonly List tabButtons = new List(); + private readonly List itemCategoryButtons = new List(); + private readonly List missionTickBoxes = new List(); + private GUIRadioButtonGroup missionRadioButtonGroup = new GUIRadioButtonGroup(); + + private Location selectedLocation; public Action StartRound; public Action OnLocationSelected; @@ -41,12 +46,12 @@ namespace Barotrauma public Level SelectedLevel { get; private set; } public GUIComponent MapContainer { get; private set; } - + public GUIButton StartButton { get; private set; } public CampaignMode Campaign { get; } - public CampaignUI(CampaignMode campaign, GUIFrame container) + public CampaignUI(CampaignMode campaign, GUIComponent container) { this.Campaign = campaign; @@ -99,7 +104,8 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.9f), tabButton.RectTransform, i == 0 ? Anchor.CenterLeft : Anchor.CenterRight) { RelativeOffset = new Vector2(0.05f, 0.0f) }, TextManager.Get(tab.ToString()), textColor: tabButton.TextColor, font: GUI.LargeFont, textAlignment: Alignment.Center, style: null) { - UserData = "buttontext" + UserData = "buttontext", + Padding = new Vector4(GUI.Scale * 1) }; } else @@ -107,7 +113,8 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.9f), tabButton.RectTransform, Anchor.Center), TextManager.Get(tab.ToString()), textColor: tabButton.TextColor, font: GUI.LargeFont, textAlignment: Alignment.Center, style: null) { - UserData = "buttontext" + UserData = "buttontext", + Padding = new Vector4(GUI.Scale * 1) }; } @@ -129,6 +136,7 @@ namespace Barotrauma }, color: Color.Black * 0.9f); new GUIFrame(new RectTransform(new Vector2(1.25f, 1.25f), tabs[(int)Tab.Crew].RectTransform, Anchor.Center), style: "OuterGlow", color: Color.Black * 0.7f) { + UserData = "outerglow", CanBeFocused = false }; @@ -174,21 +182,18 @@ namespace Barotrauma }, color: Color.Black * 0.9f); new GUIFrame(new RectTransform(new Vector2(1.25f, 1.25f), tabs[(int)Tab.Store].RectTransform, Anchor.Center), style: "OuterGlow", color: Color.Black * 0.7f) { + UserData = "outerglow", CanBeFocused = false }; - - List itemCategories = Enum.GetValues(typeof(MapEntityCategory)).Cast().ToList(); - //don't show categories with no buyable items - itemCategories.RemoveAll(c => - !MapEntityPrefab.List.Any(ep => ep.Category.HasFlag(c) && (ep is ItemPrefab) && ((ItemPrefab)ep).CanBeBought)); - + var storeContent = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), tabs[(int)Tab.Store].RectTransform, Anchor.Center)) { + UserData = "content", Stretch = true, - RelativeSpacing = 0.02f + RelativeSpacing = 0.015f }; - var storeContentTop = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), storeContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + var storeContentTop = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), storeContent.RectTransform) { MinSize = new Point(0, (int)(30 * GUI.Scale)) }, isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; @@ -197,7 +202,7 @@ namespace Barotrauma { TextGetter = GetMoney }; - var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 0.4f), storeContentTop.RectTransform), isHorizontal: true) + var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 0.4f), storeContentTop.RectTransform) { MinSize = new Point(0, (int)(25 * GUI.Scale)) }, isHorizontal: true) { Stretch = true }; @@ -214,6 +219,7 @@ namespace Barotrauma var storeItemLists = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.8f), storeContent.RectTransform), isHorizontal: true) { + RelativeSpacing = 0.03f, Stretch = true }; myItemList = new GUIListBox(new RectTransform(new Vector2(0.5f, 1.0f), storeItemLists.RectTransform)); @@ -226,6 +232,11 @@ namespace Barotrauma { RelativeSpacing = 0.02f }; + + List itemCategories = Enum.GetValues(typeof(MapEntityCategory)).Cast().ToList(); + //don't show categories with no buyable items + itemCategories.RemoveAll(c => + !MapEntityPrefab.List.Any(ep => ep.Category.HasFlag(c) && (ep is ItemPrefab) && ((ItemPrefab)ep).CanBeBought)); foreach (MapEntityCategory category in itemCategories) { var categoryButton = new GUIButton(new RectTransform(new Point(categoryButtonContainer.Rect.Width), categoryButtonContainer.RectTransform), @@ -268,6 +279,7 @@ namespace Barotrauma }, color: Color.Black * 0.9f); new GUIFrame(new RectTransform(new Vector2(1.25f, 1.25f), tabs[(int)Tab.Repair].RectTransform, Anchor.Center), style: "OuterGlow", color: Color.Black * 0.7f) { + UserData = "outerglow", CanBeFocused = false }; @@ -293,7 +305,7 @@ namespace Barotrauma IgnoreLayoutGroups = true, CanBeFocused = false }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), repairHullsHolder.RectTransform), TextManager.Get("RepairAllWalls"), textAlignment: Alignment.Right, font: GUI.LargeFont) + var repairHullsLabel = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.3f), repairHullsHolder.RectTransform), TextManager.Get("RepairAllWalls"), textAlignment: Alignment.Right, font: GUI.LargeFont) { ForceUpperCase = true }; @@ -338,7 +350,7 @@ namespace Barotrauma IgnoreLayoutGroups = true, CanBeFocused = false }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), repairItemsHolder.RectTransform), TextManager.Get("RepairAllItems"), textAlignment: Alignment.Right, font: GUI.LargeFont) + var repairItemsLabel = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.3f), repairItemsHolder.RectTransform), TextManager.Get("RepairAllItems"), textAlignment: Alignment.Right, font: GUI.LargeFont) { ForceUpperCase = true }; @@ -383,7 +395,7 @@ namespace Barotrauma IgnoreLayoutGroups = true, CanBeFocused = false }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), replaceShuttlesHolder.RectTransform), TextManager.Get("ReplaceLostShuttles"), textAlignment: Alignment.Right, font: GUI.LargeFont) + var replaceShuttlesLabel = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.3f), replaceShuttlesHolder.RectTransform), TextManager.Get("ReplaceLostShuttles"), textAlignment: Alignment.Right, font: GUI.LargeFont) { ForceUpperCase = true }; @@ -422,6 +434,7 @@ namespace Barotrauma { CanBeFocused = false }; + GUITextBlock.AutoScaleAndNormalize(repairHullsLabel, repairItemsLabel, replaceShuttlesLabel); // mission info ------------------------------------------------------------------------- @@ -436,6 +449,7 @@ namespace Barotrauma new GUIFrame(new RectTransform(new Vector2(1.25f, 1.25f), missionPanel.RectTransform, Anchor.Center), style: "OuterGlow", color: Color.Black * 0.7f) { + UserData = "outerglow", CanBeFocused = false }; @@ -443,6 +457,7 @@ namespace Barotrauma { RelativeOffset = new Vector2(0.1f, -0.05f) }, TextManager.Get("Mission"), textAlignment: Alignment.Center, font: GUI.LargeFont, style: "GUISlopedHeader") { + UserData = "missionlabel", AutoScale = true }; var missionPanelContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), missionPanel.RectTransform, Anchor.Center)) @@ -460,6 +475,11 @@ namespace Barotrauma { Visible = false }; + new GUIFrame(new RectTransform(new Vector2(1.25f, 1.25f), selectedMissionInfo.RectTransform, Anchor.Center), style: "OuterGlow", color: Color.Black * 0.9f) + { + UserData = "outerglow", + CanBeFocused = false + }; // ------------------------------------------------------------------------- @@ -473,15 +493,57 @@ namespace Barotrauma campaign.Map.OnLocationChanged += (prevLocation, newLocation) => UpdateLocationView(newLocation); campaign.Map.OnMissionSelected += (connection, mission) => { - var selectedTickBox = missionTickBoxes.Find(tb => tb.UserData == mission); - if (selectedTickBox != null) + var selectedTickBox = (missionRadioButtonGroup.UserData as List).FindIndex(m => m == mission); + if (selectedTickBox >= 0) { - selectedTickBox.Selected = true; + missionRadioButtonGroup.Selected = selectedTickBox; } }; campaign.CargoManager.OnItemsChanged += RefreshMyItems; } + public void SetMissionPanelParent(RectTransform parent) + { + missionPanel.RectTransform.Parent = parent; + missionPanel.RectTransform.RelativeOffset = Vector2.Zero; + missionPanel.RectTransform.RelativeSize = Vector2.One; + var outerGlow = missionPanel.GetChildByUserData("outerglow"); + if (outerGlow != null) { outerGlow.Visible = false; } + var label = missionPanel.GetChildByUserData("missionlabel"); + if (label != null) { label.Visible = false; } + + displayMissionPanelInMapTab = true; + + selectedMissionInfo.RectTransform.RelativeOffset = Vector2.Zero; + selectedMissionInfo.RectTransform.SetPosition(Anchor.BottomLeft, Pivot.BottomRight); + } + public void SetMenuPanelParent(RectTransform parent) + { + for (int i = 0; i < tabs.Length; i++) + { + var panel = tabs[i]; + if (panel == null) { continue; } + panel.RectTransform.Parent = parent; + panel.RectTransform.RelativeOffset = Vector2.Zero; + panel.RectTransform.RelativeSize = Vector2.One; + var outerGlow = panel.GetChildByUserData("outerglow"); + if (outerGlow != null) { outerGlow.Visible = false; } + + if (i == (int)Tab.Store) + { + panel.RectTransform.RelativeSize *= new Vector2(1.5f, 1.0f); + panel.RectTransform.SetPosition(Anchor.TopRight); + var content = panel.GetChildByUserData("content"); + if (content != null) { content.RectTransform.RelativeSize = Vector2.One; } + new GUIFrame(new RectTransform(new Vector2(1.107f, 1.0f), panel.RectTransform, Anchor.TopRight), style: null) + { + Color = Color.Black, + CanBeFocused = false + }.SetAsFirstChild(); + } + } + } + private void UpdateLocationView(Location location) { if (location == null) @@ -606,8 +668,14 @@ namespace Barotrauma public void SelectLocation(Location location, LocationConnection connection) { selectedLocationInfo.ClearChildren(); - missionPanel.Visible = location != null; - + //don't select the map panel if the tabs are displayed in the same place as the map, and we're looking at some other tab + if (!displayMissionPanelInMapTab || selectedTab == Tab.Map) + { + SelectTab(Tab.Map); + missionPanel.Visible = location != null; + } + + selectedLocation = location; if (location == null) { return; } var container = selectedLocationInfo; @@ -643,30 +711,43 @@ namespace Barotrauma Mission selectedMission = Campaign.Map.CurrentLocation.SelectedMission != null && availableMissions.Contains(Campaign.Map.CurrentLocation.SelectedMission) ? Campaign.Map.CurrentLocation.SelectedMission : null; missionTickBoxes.Clear(); - foreach (Mission mission in availableMissions) + missionRadioButtonGroup = new GUIRadioButtonGroup { + UserData = availableMissions + }; + + for (int i = 0; i < availableMissions.Count; i++) + { + var mission = availableMissions[i]; var tickBox = new GUITickBox(new RectTransform(new Vector2(0.1f, 0.1f), missionContent.RectTransform) { MaxSize = maxTickBoxSize }, - mission?.Name ?? TextManager.Get("NoMission")) + mission?.Name ?? TextManager.Get("NoMission"), style: "GUIRadioButton") { - UserData = mission, - Enabled = GameMain.Client == null || GameMain.Client.HasPermission(Networking.ClientPermissions.ManageCampaign), - Selected = mission == selectedMission, - OnSelected = (tb) => - { - if (!tb.Selected) { return false; } - RefreshMissionTab(tb.UserData as Mission); - Campaign.Map.OnMissionSelected?.Invoke(connection, mission); - if ((Campaign is MultiPlayerCampaign multiPlayerCampaign) && !multiPlayerCampaign.SuppressStateSending && - GameMain.Client != null && GameMain.Client.HasPermission(Networking.ClientPermissions.ManageCampaign)) - { - GameMain.Client?.SendCampaignState(); - } - return true; - } + Enabled = GameMain.Client == null || GameMain.Client.HasPermission(Networking.ClientPermissions.ManageCampaign) }; missionTickBoxes.Add(tickBox); + missionRadioButtonGroup.AddRadioButton(i, tickBox); } - + + if (GameMain.Client == null || GameMain.Client.HasPermission(Networking.ClientPermissions.ManageCampaign)) + { + missionRadioButtonGroup.OnSelect = (rbg, missionInd) => + { + int ind = missionInd ?? -1; + if (ind < 0) { return; } + var mission = availableMissions[ind]; + if (Campaign.Map.CurrentLocation.SelectedMission == mission) { return; } + if (rbg.Selected == missionInd) { return; } + RefreshMissionTab(mission); + if ((Campaign is MultiPlayerCampaign multiPlayerCampaign) && !multiPlayerCampaign.SuppressStateSending && + GameMain.Client != null && GameMain.Client.HasPermission(Networking.ClientPermissions.ManageCampaign)) + { + GameMain.Client?.SendCampaignState(); + } + }; + } + + missionRadioButtonGroup.Selected = availableMissions.IndexOf(selectedMission); + RefreshMissionTab(selectedMission); StartButton = new GUIButton(new RectTransform(new Vector2(0.3f, 0.7f), missionContent.RectTransform, Anchor.CenterRight), @@ -697,9 +778,10 @@ namespace Barotrauma GameMain.GameSession.Map.CurrentLocation.SelectedMission = selectedMission; - foreach (GUITickBox missionTickBox in missionTickBoxes) + var selectedTickBoxIndex = (missionRadioButtonGroup.UserData as List).FindIndex(m => m == selectedMission); + if (selectedTickBoxIndex >= 0) { - missionTickBox.Selected = missionTickBox.UserData == selectedMission; + missionRadioButtonGroup.Selected = selectedTickBoxIndex; } selectedMissionInfo.ClearChildren(); @@ -734,7 +816,7 @@ namespace Barotrauma } } - private GUIComponent CreateItemFrame(PurchasedItem pi, PriceInfo priceInfo, GUIListBox listBox, int width) + private GUIComponent CreateItemFrame(PurchasedItem pi, PriceInfo priceInfo, GUIListBox listBox) { GUIFrame frame = new GUIFrame(new RectTransform(new Point(listBox.Content.Rect.Width, (int)(GUI.Scale * 50)), listBox.Content.RectTransform), style: "ListBoxElement") { @@ -865,7 +947,7 @@ namespace Barotrauma var itemFrame = myItemList.Content.GetChildByUserData(pi); if (itemFrame == null) { - itemFrame = CreateItemFrame(pi, pi.ItemPrefab.GetPrice(Campaign.Map.CurrentLocation), myItemList, myItemList.Rect.Width); + itemFrame = CreateItemFrame(pi, pi.ItemPrefab.GetPrice(Campaign.Map.CurrentLocation), myItemList); } itemFrame.GetChild(0).GetChild().IntValue = pi.Quantity; existingItemFrames.Add(itemFrame); @@ -894,6 +976,9 @@ namespace Barotrauma tabs[i].Visible = (int)selectedTab == i; } } + + missionPanel.Visible = tab == Tab.Map && selectedLocation != null; + foreach (GUIButton button in tabButtons) { button.Selected = (Tab)button.UserData == tab; @@ -932,7 +1017,6 @@ namespace Barotrauma float prevStoreItemScroll = storeItemList.BarScroll; float prevMyItemScroll = myItemList.BarScroll; - int width = storeItemList.Rect.Width; HashSet existingItemFrames = new HashSet(); foreach (MapEntityPrefab mapEntityPrefab in MapEntityPrefab.List) { @@ -943,7 +1027,7 @@ namespace Barotrauma var itemFrame = myItemList.Content.GetChildByUserData(priceInfo); if (itemFrame == null) { - itemFrame = CreateItemFrame(new PurchasedItem(itemPrefab, 0), priceInfo, storeItemList, width); + itemFrame = CreateItemFrame(new PurchasedItem(itemPrefab, 0), priceInfo, storeItemList); } existingItemFrames.Add(itemFrame); } @@ -1005,7 +1089,7 @@ namespace Barotrauma if (characterPreviewFrame == null || characterPreviewFrame.UserData != characterInfo) { - characterPreviewFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.5f), tabs[(int)selectedTab].RectTransform, Anchor.TopRight, Pivot.TopLeft)) + characterPreviewFrame = new GUIFrame(new RectTransform(new Vector2(0.75f, 0.5f), tabs[(int)selectedTab].RectTransform, Anchor.TopRight, Pivot.TopLeft)) { UserData = characterInfo }; diff --git a/Barotrauma/BarotraumaClient/Source/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/Source/Screens/CharacterEditor/CharacterEditorScreen.cs index a30c09a00..873606255 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -121,6 +121,7 @@ namespace Barotrauma.CharacterEditor ResetVariables(); Submarine.MainSub = new Submarine("Content/AnimEditor.sub"); Submarine.MainSub.Load(unloadPrevious: false, showWarningMessages: false); + Submarine.MainSub.PhysicsBody.Enabled = false; originalWall = new WallGroup(new List(Structure.WallList)); CloneWalls(); CalculateMovementLimits(); @@ -716,13 +717,13 @@ namespace Barotrauma.CharacterEditor limbEditWidgets.Values.ForEach(w => w.Update((float)deltaTime)); animationWidgets.Values.ForEach(w => w.Update((float)deltaTime)); // Handle limb selection - if (editLimbs && PlayerInput.LeftButtonDown() && GUI.MouseOn == null && Widget.selectedWidgets.None()) + if (PlayerInput.LeftButtonDown() && GUI.MouseOn == null && Widget.selectedWidgets.None()) { foreach (Limb limb in character.AnimController.Limbs) { if (limb == null || limb.ActiveSprite == null) { continue; } // Select limbs on ragdoll - if (!spriteSheetRect.Contains(PlayerInput.MousePosition) && MathUtils.RectangleContainsPoint(GetLimbPhysicRect(limb), PlayerInput.MousePosition)) + if (editLimbs && !spriteSheetRect.Contains(PlayerInput.MousePosition) && MathUtils.RectangleContainsPoint(GetLimbPhysicRect(limb), PlayerInput.MousePosition)) { HandleLimbSelection(limb); } @@ -795,7 +796,7 @@ namespace Barotrauma.CharacterEditor } // GUI - spriteBatch.Begin(SpriteSortMode.Deferred, rasterizerState: GameMain.ScissorTestEnable); + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); if (drawDamageModifiers) { foreach (Limb limb in character.AnimController.Limbs) @@ -926,14 +927,14 @@ namespace Barotrauma.CharacterEditor { UpdateOtherLimbs(lastLimb, l => TryUpdateSubParam(l.Params, "spriteorientation", angle)); } - }, circleRadius: 40, widgetSize: 15, rotationOffset: MathHelper.Pi, autoFreeze: false); + }, circleRadius: 40, widgetSize: 15, rotationOffset: MathHelper.Pi, autoFreeze: false, rounding: 10); } else { var topLeft = spriteSheetControls.RectTransform.TopLeft; GUI.DrawString(spriteBatch, new Vector2(topLeft.X + 350 * GUI.xScale, GameMain.GraphicsHeight - 95 * GUI.yScale), GetCharacterEditorTranslation("SpriteSheetOrientation") + ":", Color.White, Color.Gray * 0.5f, 10, GUI.Font); DrawRadialWidget(spriteBatch, new Vector2(topLeft.X + 560 * GUI.xScale, GameMain.GraphicsHeight - 75 * GUI.yScale), RagdollParams.SpritesheetOrientation, string.Empty, Color.White, - angle => TryUpdateRagdollParam("spritesheetorientation", angle), circleRadius: 40, widgetSize: 15, rotationOffset: MathHelper.Pi, autoFreeze: false); + angle => TryUpdateRagdollParam("spritesheetorientation", angle), circleRadius: 40, widgetSize: 15, rotationOffset: MathHelper.Pi, autoFreeze: false, rounding: 10); } } // Debug @@ -1763,7 +1764,7 @@ namespace Barotrauma.CharacterEditor { case AnimationType.Walk: case AnimationType.Run: - if (!ragdollParams.CanEnterSubmarine) { continue; } + if (!ragdollParams.CanWalk) { continue; } break; case AnimationType.SwimSlow: case AnimationType.SwimFast: @@ -1903,7 +1904,7 @@ namespace Barotrauma.CharacterEditor }; Vector2 buttonSize = new Vector2(1, 0.04f); - Vector2 toggleSize = new Vector2(0.03f, 0.03f); + Vector2 toggleSize = new Vector2(1.0f, 0.03f); CreateCharacterSelectionPanel(); CreateMinorModesPanel(toggleSize); @@ -3212,6 +3213,10 @@ namespace Barotrauma.CharacterEditor { CreateCloseButton(emitter.SerializableEntityEditor, () => CharacterParams.RemoveGibEmitter(emitter)); } + foreach (var emitter in CharacterParams.DamageEmitters) + { + CreateCloseButton(emitter.SerializableEntityEditor, () => CharacterParams.RemoveDamageEmitter(emitter)); + } foreach (var sound in CharacterParams.Sounds) { CreateCloseButton(sound.SerializableEntityEditor, () => CharacterParams.RemoveSound(sound)); @@ -3228,6 +3233,7 @@ namespace Barotrauma.CharacterEditor } CreateAddButtonAtLast(mainEditor, () => CharacterParams.AddBloodEmitter(), GetCharacterEditorTranslation("AddBloodEmitter")); CreateAddButtonAtLast(mainEditor, () => CharacterParams.AddGibEmitter(), GetCharacterEditorTranslation("AddGibEmitter")); + CreateAddButtonAtLast(mainEditor, () => CharacterParams.AddDamageEmitter(), GetCharacterEditorTranslation("AddDamageEmitter")); CreateAddButtonAtLast(mainEditor, () => CharacterParams.AddSound(), GetCharacterEditorTranslation("AddSound")); CreateAddButtonAtLast(mainEditor, () => CharacterParams.AddInventory(), GetCharacterEditorTranslation("AddInventory")); } @@ -3590,6 +3596,10 @@ namespace Barotrauma.CharacterEditor private void HandleLimbSelection(Limb limb) { + if (!editLimbs) + { + SetToggle(limbsToggle, true); + } if (!selectedLimbs.Contains(limb)) { if (!Widget.EnableMultiSelect) @@ -4103,7 +4113,7 @@ namespace Barotrauma.CharacterEditor // Fish swim only --> else if (tail != null && fishSwimParams != null) { - float amplitudeMultiplier = 0.5f; + float amplitudeMultiplier = 20; float lengthMultiplier = 20; int points = 1000; float GetAmplitude() => ConvertUnits.ToDisplayUnits(fishSwimParams.WaveAmplitude) * Cam.Zoom / amplitudeMultiplier; @@ -4120,7 +4130,7 @@ namespace Barotrauma.CharacterEditor w.MouseHeld += dTime => { float input = Vector2.Multiply(ConvertUnits.ToSimUnits(PlayerInput.MouseSpeed), GetScreenSpaceForward()).Combine() / Cam.Zoom * lengthMultiplier; - TryUpdateAnimParam("wavelength", MathHelper.Clamp(fishSwimParams.WaveLength - input, 0, 150)); + TryUpdateAnimParam("wavelength", MathHelper.Clamp(fishSwimParams.WaveLength - input, 0, 200)); }; // Additional w.PreDraw += (sp, dTime) => @@ -4138,7 +4148,7 @@ namespace Barotrauma.CharacterEditor w.MouseHeld += dTime => { float input = Vector2.Multiply(ConvertUnits.ToSimUnits(PlayerInput.MouseSpeed), GetScreenSpaceForward().Right()).Combine() * character.AnimController.Dir / Cam.Zoom * amplitudeMultiplier; - TryUpdateAnimParam("waveamplitude", MathHelper.Clamp(fishSwimParams.WaveAmplitude + input, -4, 4)); + TryUpdateAnimParam("waveamplitude", MathHelper.Clamp(fishSwimParams.WaveAmplitude + input, -100, 100)); }; // Additional w.PreDraw += (sp, dTime) => @@ -4407,7 +4417,7 @@ namespace Barotrauma.CharacterEditor DrawJointLimitWidgets(spriteBatch, limb, joint, tformedJointPos, autoFreeze: true, allowPairEditing: true, rotationOffset: limb.Rotation, holdPosition: true); } // Is the direction inversed incorrectly? - Vector2 to = tformedJointPos + VectorExtensions.ForwardFlipped(joint.LimbB.Rotation + MathHelper.ToRadians(-joint.LimbB.Params.GetSpriteOrientation()), 20); + Vector2 to = tformedJointPos + VectorExtensions.ForwardFlipped(joint.LimbB.Rotation - joint.LimbB.Params.GetSpriteOrientation(), 20); GUI.DrawLine(spriteBatch, tformedJointPos, to, Color.Magenta, width: 2); var dotSize = new Vector2(5, 5); var rect = new Rectangle((tformedJointPos - dotSize / 2).ToPoint(), dotSize.ToPoint()); @@ -5010,7 +5020,7 @@ namespace Barotrauma.CharacterEditor private void DrawJointLimitWidgets(SpriteBatch spriteBatch, Limb limb, LimbJoint joint, Vector2 drawPos, bool autoFreeze, bool allowPairEditing, bool holdPosition, float rotationOffset = 0) { - rotationOffset += limb.Params.GetSpriteOrientation(); + rotationOffset -= limb.Params.GetSpriteOrientation(); Color angleColor = joint.UpperLimit - joint.LowerLimit > 0 ? Color.LightGreen * 0.5f : Color.Red; DrawRadialWidget(spriteBatch, drawPos, MathHelper.ToDegrees(joint.UpperLimit), $"{joint.Params.Name}: {GetCharacterEditorTranslation("UpperLimit")}", Color.Cyan, angle => { @@ -5176,7 +5186,7 @@ namespace Barotrauma.CharacterEditor #region Widgets as methods private void DrawRadialWidget(SpriteBatch spriteBatch, Vector2 drawPos, float value, string toolTip, Color color, Action onClick, - float circleRadius = 30, int widgetSize = 10, float rotationOffset = 0, bool clockWise = true, bool displayAngle = true, bool? autoFreeze = null, bool wrapAnglePi = false, bool holdPosition = false) + float circleRadius = 30, int widgetSize = 10, float rotationOffset = 0, bool clockWise = true, bool displayAngle = true, bool? autoFreeze = null, bool wrapAnglePi = false, bool holdPosition = false, int rounding = 1) { var angle = value; if (!MathUtils.IsValid(angle)) @@ -5195,6 +5205,8 @@ namespace Barotrauma.CharacterEditor ? MathUtils.VectorToAngle(d) - MathHelper.PiOver2 + rotationOffset : -MathUtils.VectorToAngle(d) + MathHelper.PiOver2 - rotationOffset; angle = MathHelper.ToDegrees(wrapAnglePi ? MathUtils.WrapAnglePi(newAngle) : MathUtils.WrapAngleTwoPi(newAngle)); + angle = (float)Math.Round(angle / rounding) * rounding; + if (angle >= 360 || angle <= -360) { angle = 0; } if (displayAngle) { GUI.DrawString(spriteBatch, drawPos, angle.FormatZeroDecimal(), Color.Black, backgroundColor: color, font: GUI.SmallFont); diff --git a/Barotrauma/BarotraumaClient/Source/Screens/CharacterEditor/Wizard.cs b/Barotrauma/BarotraumaClient/Source/Screens/CharacterEditor/Wizard.cs index 872733f3c..b14c551f4 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/CharacterEditor/Wizard.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/CharacterEditor/Wizard.cs @@ -15,6 +15,7 @@ namespace Barotrauma.CharacterEditor private string name; private bool isHumanoid; private bool canEnterSubmarine = true; + private bool canWalk; private string texturePath; private string xmlPath; private ContentPackage contentPackage; @@ -37,6 +38,7 @@ namespace Barotrauma.CharacterEditor name = character.SpeciesName; isHumanoid = character.Humanoid; canEnterSubmarine = ragdoll.CanEnterSubmarine; + canWalk = ragdoll.CanWalk; texturePath = ragdoll.Texture; } @@ -165,7 +167,7 @@ namespace Barotrauma.CharacterEditor texturePathElement.Text = TexturePath; } } - for (int i = 0; i < 6; i++) + for (int i = 0; i < 7; i++) { var mainElement = new GUIFrame(new RectTransform(new Point(topGroup.RectTransform.Rect.Width, elementSize), topGroup.RectTransform), style: null, color: Color.Gray * 0.25f); fields.Add(mainElement); @@ -212,6 +214,19 @@ namespace Barotrauma.CharacterEditor } break; case 3: + var lbl = new GUITextBlock(leftElement, GetCharacterEditorTranslation("CanWalk")); + var txt = new GUITickBox(rightElement, string.Empty) + { + Selected = CanWalk, + Enabled = !IsCopy, + OnSelected = (tB) => CanWalk = tB.Selected + }; + if (!txt.Enabled) + { + lbl.TextColor *= 0.6f; + } + break; + case 4: new GUITextBlock(leftElement, GetCharacterEditorTranslation("ConfigFileOutput")); xmlPathElement = new GUITextBox(rightElement, string.Empty) { @@ -224,7 +239,7 @@ namespace Barotrauma.CharacterEditor return true; }; break; - case 4: + case 5: //new GUITextBlock(leftElement, GetCharacterEditorTranslation("TexturePath")); texturePathElement = new GUITextBox(rightElement, string.Empty) { @@ -257,7 +272,7 @@ namespace Barotrauma.CharacterEditor } }; break; - case 5: + case 6: mainElement.RectTransform.NonScaledSize = new Point( mainElement.RectTransform.NonScaledSize.X, mainElement.RectTransform.NonScaledSize.Y * 2); @@ -356,6 +371,7 @@ namespace Barotrauma.CharacterEditor { SourceRagdoll.Texture = TexturePath; SourceRagdoll.CanEnterSubmarine = CanEnterSubmarine; + SourceRagdoll.CanWalk = CanWalk; SourceRagdoll.Serialize(); Wizard.Instance.CreateCharacter(SourceRagdoll.MainElement, SourceCharacter.MainElement, SourceAnimations); } @@ -754,6 +770,7 @@ namespace Barotrauma.CharacterEditor new XAttribute("type", Name), new XAttribute("texture", TexturePath), new XAttribute("canentersubmarine", CanEnterSubmarine), + new XAttribute("canwalk", CanWalk), colliderElements, LimbXElements.Values, JointXElements); @@ -873,6 +890,11 @@ namespace Barotrauma.CharacterEditor get => Instance.canEnterSubmarine; set => Instance.canEnterSubmarine = value; } + public bool CanWalk + { + get => Instance.canWalk; + set => Instance.canWalk = value; + } public ContentPackage ContentPackage { get => Instance.contentPackage; diff --git a/Barotrauma/BarotraumaClient/Source/Screens/CreditsPlayer.cs b/Barotrauma/BarotraumaClient/Source/Screens/CreditsPlayer.cs index 588220be6..7e0cdd03a 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/CreditsPlayer.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/CreditsPlayer.cs @@ -37,7 +37,7 @@ namespace Barotrauma { GUIComponent.FromXML(subElement, listBox.Content.RectTransform); } - foreach (GUIComponent child in listBox.Children) + foreach (GUIComponent child in listBox.Content.Children) { child.CanBeFocused = false; } diff --git a/Barotrauma/BarotraumaClient/Source/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/Source/Screens/GameScreen.cs index 58712b928..4bce025ca 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/GameScreen.cs @@ -94,7 +94,7 @@ namespace Barotrauma GameMain.PerformanceCounter.AddElapsedTicks("DrawMap", sw.ElapsedTicks); sw.Restart(); - spriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, GameMain.ScissorTestEnable); + spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); if (Character.Controlled != null && cam != null) Character.Controlled.DrawHUD(spriteBatch, cam); diff --git a/Barotrauma/BarotraumaClient/Source/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/Source/Screens/LevelEditorScreen.cs index bac9172fd..2285b5601 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/LevelEditorScreen.cs @@ -471,7 +471,7 @@ namespace Barotrauma GameMain.SpriteEditorScreen.Draw(deltaTime, graphics, spriteBatch); } - spriteBatch.Begin(SpriteSortMode.Deferred, rasterizerState: GameMain.ScissorTestEnable); + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); GUI.Draw(Cam, spriteBatch); spriteBatch.End(); } diff --git a/Barotrauma/BarotraumaClient/Source/Screens/LobbyScreen.cs b/Barotrauma/BarotraumaClient/Source/Screens/LobbyScreen.cs index 3ea6a22fe..06c36a881 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/LobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/LobbyScreen.cs @@ -55,7 +55,7 @@ namespace Barotrauma GUI.DrawBackgroundSprite(spriteBatch, GameMain.GameSession.Map.CurrentLocation.Type.GetPortrait(GameMain.GameSession.Map.CurrentLocation.PortraitId)); - spriteBatch.Begin(SpriteSortMode.Deferred, rasterizerState: GameMain.ScissorTestEnable); + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); GUI.Draw(Cam, spriteBatch); spriteBatch.End(); } diff --git a/Barotrauma/BarotraumaClient/Source/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/Source/Screens/MainMenuScreen.cs index 9e03d9234..c41608a1c 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/MainMenuScreen.cs @@ -1,4 +1,6 @@ -using Barotrauma.Extensions; +//#define TEST_REMOTE_CONTENT + +using Barotrauma.Extensions; using Barotrauma.Networking; using Barotrauma.Tutorials; using Lidgren.Network; @@ -19,27 +21,28 @@ namespace Barotrauma { public enum Tab { NewGame = 1, LoadGame = 2, HostServer = 3, Settings = 4, Tutorials = 5, JoinServer = 6, CharacterEditor = 7, SubmarineEditor = 8, QuickStartDev = 9, SteamWorkshop = 10, Credits = 11, Empty = 12 } - private GUIComponent buttonsParent; + private readonly GUIComponent buttonsParent; private readonly GUIFrame[] menuTabs; - private CampaignSetupUI campaignSetupUI; + private readonly CampaignSetupUI campaignSetupUI; private GUITextBox serverNameBox, /*portBox, queryPortBox,*/ passwordBox, maxPlayersBox; private GUITickBox isPublicBox/*, useUpnpBox*/; + private readonly GUIButton joinServerButton, hostServerButton, steamWorkshopButton; + private readonly GameMain game; - private GUIButton joinServerButton, hostServerButton, steamWorkshopButton; - - private GameMain game; + private GUIImage playstyleBanner; + private GUITextBlock playstyleDescription; private Tab selectedTab; private Sprite backgroundSprite; private Sprite backgroundVignette; - private GUIComponent titleText; + private readonly GUIComponent titleText; - private CreditsPlayer creditsPlayer; + private readonly CreditsPlayer creditsPlayer; #if OSX private bool firstLoadOnMac = true; @@ -70,15 +73,20 @@ namespace Barotrauma RelativeSpacing = 0.02f }; - FetchRemoteContent(Frame.RectTransform); - /*var doc = XMLExtensions.TryLoadXml("Content/UI/MenuTextTest.xml"); +#if TEST_REMOTE_CONTENT + + var doc = XMLExtensions.TryLoadXml("Content/UI/MenuTextTest.xml"); if (doc?.Root != null) { foreach (XElement subElement in doc?.Root.Elements()) { GUIComponent.FromXML(subElement, Frame.RectTransform); } - }*/ + } +#else + FetchRemoteContent(Frame.RectTransform); +#endif + // === CAMPAIGN var campaignHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 1.0f), parent: buttonsParent.RectTransform) { RelativeOffset = new Vector2(0.1f, 0.0f) }, isHorizontal: true); @@ -331,7 +339,7 @@ namespace Barotrauma StartNewGame = StartGame }; - var hostServerScale = new Vector2(0.7f, 0.6f); + var hostServerScale = new Vector2(0.7f, 1.2f); menuTabs[(int)Tab.HostServer] = new GUIFrame(new RectTransform( Vector2.Multiply(relativeSize, hostServerScale), GUI.Canvas, anchor, pivot, minSize.Multiply(hostServerScale), maxSize.Multiply(hostServerScale)) { RelativeOffset = relativeSpacing }); @@ -374,9 +382,9 @@ namespace Barotrauma }; } - #endregion +#endregion - #region Selection +#region Selection public override void Select() { base.Select(); @@ -393,7 +401,7 @@ namespace Barotrauma GameAnalyticsManager.SetCustomDimension01(""); - #if OSX +#if OSX // Hack for adjusting the viewport properly after splash screens on older Macs if (firstLoadOnMac) { @@ -410,7 +418,7 @@ namespace Barotrauma SelectTab(null, Tab.Empty); } - #endif +#endif } public override void Deselect() @@ -482,6 +490,7 @@ namespace Barotrauma GameMain.ServerListScreen.Select(); break; case Tab.HostServer: + SetServerPlayStyle(PlayStyle.Serious); if (!GameMain.Config.CampaignDisclaimerShown) { selectedTab = 0; @@ -661,13 +670,15 @@ namespace Barotrauma { GameMain.Config.SaveNewPlayerConfig(); - if (userData is Tab) SelectTab(button, (Tab)userData); + if (userData is Tab) { SelectTab(button, (Tab)userData); } - if (GameMain.GraphicsWidth != GameMain.Config.GraphicsWidth || GameMain.GraphicsHeight != GameMain.Config.GraphicsHeight) + if (GameMain.GraphicsWidth != GameMain.Config.GraphicsWidth || + GameMain.GraphicsHeight != GameMain.Config.GraphicsHeight || + ContentPackage.List.Any(cp => cp.NeedsRestart)) { new GUIMessageBox( TextManager.Get("RestartRequiredLabel"), - TextManager.Get("RestartRequiredText")); + TextManager.Get("RestartRequiredGeneric")); } return true; @@ -744,6 +755,7 @@ namespace Barotrauma string arguments = "-name \"" + name.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"" + " -public " + isPublicBox.Selected.ToString() + + " -playstyle " + ((PlayStyle)playstyleBanner.UserData).ToString() + " -password \"" + passwordBox.Text.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"" + " -maxplayers " + maxPlayersBox.Text; @@ -856,7 +868,7 @@ namespace Barotrauma { DrawBackground(graphics, spriteBatch); - spriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, GameMain.ScissorTestEnable); + spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); GUI.Draw(Cam, spriteBatch); @@ -981,22 +993,79 @@ namespace Barotrauma Alignment textAlignment = Alignment.CenterLeft; Vector2 textFieldSize = new Vector2(0.5f, 1.0f); Vector2 tickBoxSize = new Vector2(0.4f, 0.07f); - var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.85f, 0.75f), menuTabs[(int)Tab.HostServer].RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.05f) }) + var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.85f, 0.8f), menuTabs[(int)Tab.HostServer].RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.05f) }) { RelativeSpacing = 0.02f, Stretch = true }; GUIComponent parent = paddedFrame; - + new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), TextManager.Get("HostServerButton"), textAlignment: Alignment.Center, font: GUI.LargeFont) { ForceUpperCase = true }; + //play style ----------------------------------------------------- + + var playstyleContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), parent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + { + Stretch = true, + Color = Color.Black + //RelativeSpacing = 0.02f + }; + + new GUIButton(new RectTransform(new Vector2(0.05f, 1.0f), playstyleContainer.RectTransform), style: "UIToggleButton") + { + OnClicked = (btn, userdata) => + { + int playStyleIndex = (int)playstyleBanner.UserData - 1; + if (playStyleIndex < 0) { playStyleIndex = Enum.GetValues(typeof(PlayStyle)).Length - 1; } + SetServerPlayStyle((PlayStyle)playStyleIndex); + return true; + } + }.Children.ForEach(c => c.SpriteEffects = SpriteEffects.FlipHorizontally); + + playstyleBanner = new GUIImage(new RectTransform(new Vector2(0.8f, 1.0f), playstyleContainer.RectTransform), style: null, scaleToFit: true) + { + UserData = PlayStyle.Serious + }; + new GUITextBlock(new RectTransform(new Vector2(0.15f, 0.05f), playstyleBanner.RectTransform) { RelativeOffset = new Vector2(0.01f, 0.06f) }, + "playstyle name goes here", font: GUI.SmallFont, textAlignment: Alignment.Center, textColor: Color.White, style: "GUISlopedHeader"); + + new GUIButton(new RectTransform(new Vector2(0.05f, 1.0f), playstyleContainer.RectTransform), style: "UIToggleButton") + { + OnClicked = (btn, userdata) => + { + int playStyleIndex = (int)playstyleBanner.UserData + 1; + if (playStyleIndex >= Enum.GetValues(typeof(PlayStyle)).Length) { playStyleIndex = 0; } + SetServerPlayStyle((PlayStyle)playStyleIndex); + return true; + } + }; + + string longestPlayStyleStr = ""; + foreach (PlayStyle playStyle in Enum.GetValues(typeof(PlayStyle))) + { + string playStyleStr = TextManager.Get("servertagdescription." + playStyle); + if (playStyleStr.Length > longestPlayStyleStr.Length) { longestPlayStyleStr = playStyleStr; } + } + + playstyleDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), playstyleBanner.RectTransform, Anchor.BottomCenter), + "playstyle description goes here", style: null, wrap: true) + { + Color = Color.Black * 0.8f, + TextColor = Color.White + }; + playstyleDescription.Padding = Vector4.One * 10.0f * GUI.Scale; + playstyleDescription.CalculateHeightFromText(padding: (int)(15 * GUI.Scale)); + playstyleDescription.RectTransform.MinSize = new Point(0, playstyleDescription.Rect.Height); + + //other settings ----------------------------------------------------- + var label = new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), TextManager.Get("ServerName"), textAlignment: textAlignment); serverNameBox = new GUITextBox(new RectTransform(textFieldSize, label.RectTransform, Anchor.CenterRight), textAlignment: textAlignment) { MaxTextLength = NetConfig.ServerNameMaxLength, OverflowClip = true }; - + /* TODO: allow lidgren servers from client? label = new GUITextBlock(new RectTransform(textLabelSize, parent.RectTransform), TextManager.Get("ServerPort"), textAlignment: textAlignment); portBox = new GUITextBox(new RectTransform(textFieldSize, label.RectTransform, Anchor.CenterRight), textAlignment: textAlignment) @@ -1064,7 +1133,21 @@ namespace Barotrauma OnClicked = HostServerClicked }; } - #endregion + + private void SetServerPlayStyle(PlayStyle playStyle) + { + playstyleBanner.Sprite = GameMain.ServerListScreen.PlayStyleBanners[(int)playStyle]; + playstyleBanner.UserData = playStyle; + + var nameText = playstyleBanner.GetChild(); + nameText.Text = TextManager.AddPunctuation(':', TextManager.Get("serverplaystyle"), TextManager.Get("servertag." + playStyle)); + nameText.Color = GameMain.ServerListScreen.PlayStyleColors[(int)playStyle]; + nameText.RectTransform.NonScaledSize = (nameText.Font.MeasureString(nameText.Text) + new Vector2(25, 10) * GUI.Scale).ToPoint(); + + playstyleDescription.Text = TextManager.Get("servertagdescription." + playStyle); + playstyleDescription.CalculateHeightFromText(padding: (int)(15 * GUI.Scale)); + } +#endregion private void FetchRemoteContent(RectTransform parent) { diff --git a/Barotrauma/BarotraumaClient/Source/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/Source/Screens/NetLobbyScreen.cs index bc8f6711e..39f80d712 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/NetLobbyScreen.cs @@ -4,79 +4,95 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Xml.Linq; namespace Barotrauma { partial class NetLobbyScreen : Screen { - private GUIFrame infoFrame, chatFrame, playerListFrame; + private readonly List characterSprites = new List(); + private readonly List jobPreferenceSprites = new List(); + + private GUIFrame infoFrame, modeFrame; private GUIFrame myCharacterFrame; - private GUIListBox playerList; - private GUIListBox subList, modeList, chatBox; - public GUIListBox ChatBox + private GUIListBox subList, modeList; + + private GUIListBox chatBox, playerList; + private GUIListBox serverLogBox, serverLogFilterTicks; + + private GUITextBox chatInput; + private GUITextBox serverLogFilter; + public GUITextBox ChatInput { get { - return chatBox; + return chatInput; } } - private GUIScrollBar levelDifficultyScrollBar; + private readonly GUIImage micIcon; - private GUIButton[] traitorProbabilityButtons; - private GUITextBlock traitorProbabilityText; + private readonly GUIScrollBar levelDifficultyScrollBar; - private GUIButton[] botCountButtons; - private GUITextBlock botCountText; + private readonly GUIButton[] traitorProbabilityButtons; + private readonly GUITextBlock traitorProbabilityText; - private GUIButton[] botSpawnModeButtons; - private GUITextBlock botSpawnModeText; + private readonly GUIButton[] botCountButtons; + private readonly GUITextBlock botCountText; - private GUIButton[] missionTypeButtons; - private GUIComponent missionTypeContainer; + private readonly GUIButton[] botSpawnModeButtons; + private readonly GUITextBlock botSpawnModeText; - private GUIListBox jobList; + public readonly GUIFrame MissionTypeFrame; + public readonly GUIFrame CampaignSetupFrame; - private GUITextBox textBox, seedBox; - public GUITextBox TextBox - { - get - { - return textBox; - } - } + private readonly GUITickBox[] missionTypeTickBoxes; + private readonly GUIListBox missionTypeList; + + public GUITextBox SeedBox { - get - { - return seedBox; - } + get; private set; } - private GUIFrame defaultModeContainer, campaignContainer; - private GUIButton campaignViewButton, spectateButton; + private readonly GUIComponent gameModeContainer, campaignContainer; + private readonly GUIButton gameModeViewButton, campaignViewButton, spectateButton; + private readonly GUILayoutGroup roundControlsHolder; public GUIButton SettingsButton { get; private set; } - private GUITickBox playYourself; + private readonly GUITickBox spectateBox; - private GUIFrame playerInfoContainer; + private readonly GUIFrame playerInfoContainer; private GUIButton jobInfoFrame; private GUIButton playerFrame; - private GUITickBox autoRestartBox; + private readonly GUIComponent subPreviewContainer; + + private readonly GUITickBox autoRestartBox; + private readonly GUITextBlock autoRestartText; private GUIDropDown shuttleList; private GUITickBox shuttleTickBox; private CampaignUI campaignUI; - public GUIComponent CampaignSetupUI; private Sprite backgroundSprite; - private GUIButton faceSelectionLeft; - private GUIButton faceSelectionRight; + private GUIButton jobPreferencesButton; + private GUIButton appearanceButton; + + private GUIFrame characterInfoFrame; + private GUIFrame appearanceFrame; + + public GUIListBox HeadSelectionList; + public GUIFrame JobSelectionFrame; + + public GUIListBox JobList; + + private Rectangle[] voipSheetRects; private float autoRestartTimer; @@ -90,9 +106,15 @@ namespace Barotrauma } //elements that can only be used by the host - private List clientDisabledElements = new List(); + private readonly List clientDisabledElements = new List(); //elements that aren't shown client-side - private List clientHiddenElements = new List(); + private readonly List clientHiddenElements = new List(); + + public GUIComponent FileTransferFrame { get; private set; } + public GUITextBlock FileTransferTitle { get; private set; } + public GUIProgressBar FileTransferProgressBar { get; private set; } + public GUITextBlock FileTransferProgressText { get; private set; } + private bool AllowSubSelection { @@ -108,20 +130,22 @@ namespace Barotrauma get; private set; } - - + public GUITextBox ServerMessage { get; private set; } - - public GUIButton ShowLogButton + + public GUILayoutGroup LogButtons { get; private set; } + private GUIButton showChatButton; + private GUIButton showLogButton; + public GUIListBox SubList { get { return subList; } @@ -136,10 +160,28 @@ namespace Barotrauma { get { return modeList; } } + + private int selectedModeIndex; public int SelectedModeIndex + { + get { return selectedModeIndex; } + set + { + if (HighlightedModeIndex == selectedModeIndex) + { + modeList.Select(value); + } + selectedModeIndex = value; + } + } + + public int HighlightedModeIndex { get { return modeList.SelectedIndex; } - set { modeList.Select(value); } + set + { + modeList.Select(value, true); + } } public GUIListBox PlayerList @@ -165,10 +207,6 @@ namespace Barotrauma private set; } - public GUIFrame InfoFrame - { - get { return infoFrame; } - } public Submarine SelectedSub { @@ -192,27 +230,55 @@ namespace Barotrauma get { return modeList.SelectedData as GameModePreset; } } - public int MissionTypeIndex + public MissionType MissionType { - get { return (int)missionTypeContainer.UserData; } - set { missionTypeContainer.UserData = value; } + get + { + MissionType retVal = MissionType.None; + int index = 0; + foreach (MissionType type in Enum.GetValues(typeof(MissionType))) + { + if (type == MissionType.None || type == MissionType.All) { continue; } + + if (missionTypeTickBoxes[index].Selected) + { + retVal = (MissionType)((int)retVal | (int)type); + } + + index++; + } + + return retVal; + } + set + { + int index = 0; + foreach (MissionType type in Enum.GetValues(typeof(MissionType))) + { + if (type == MissionType.None || type == MissionType.All) { continue; } + + missionTypeTickBoxes[index].Selected = (((int)type & (int)value) != 0); + + index++; + } + } } - - public List JobPreferences + + public List> JobPreferences { get { //joblist if the server has already assigned the player a job //(e.g. the player has a pre-existing campaign character) - if (jobList?.Content == null) + if (JobList?.Content == null) { - return new List(); + return new List>(); } - List jobPreferences = new List(); - foreach (GUIComponent child in jobList.Content.Children) + List> jobPreferences = new List>(); + foreach (GUIComponent child in JobList.Content.Children) { - JobPrefab jobPrefab = child.UserData as JobPrefab; + var jobPrefab = child.UserData as Pair; if (jobPrefab == null) continue; jobPreferences.Add(jobPrefab); } @@ -234,24 +300,10 @@ namespace Barotrauma int intSeed = ToolBox.StringToInt(levelSeed); backgroundSprite = LocationType.Random(new MTRandom(intSeed))?.GetPortrait(intSeed); - seedBox.Text = levelSeed; - - //lastUpdateID++; + SeedBox.Text = levelSeed; } } - public string AutoRestartText() - { - /*TODO: fix? - if (GameMain.Server != null) - { - if (!GameMain.Server.AutoRestart || GameMain.Server.ConnectedClients.Count == 0) return ""; - return TextManager.Get("RestartingIn") + " " + ToolBox.SecondsToReadableTime(Math.Max(GameMain.Server.AutoRestartTimer, 0)); - }*/ - - if (autoRestartTimer == 0.0f) return ""; - return TextManager.Get("RestartingIn") + " " + ToolBox.SecondsToReadableTime(Math.Max(autoRestartTimer, 0)); - } public CampaignUI CampaignUI { @@ -260,83 +312,355 @@ namespace Barotrauma public NetLobbyScreen() { - defaultModeContainer = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.95f), Frame.RectTransform, Anchor.Center) { MaxSize = new Point(int.MaxValue, GameMain.GraphicsHeight - 100) }, style: null); - campaignContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.75f), Frame.RectTransform, Anchor.TopCenter), style: null) + float panelSpacing = 0.005f; + var innerFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), Frame.RectTransform, Anchor.Center) { MaxSize = new Point(int.MaxValue, GameMain.GraphicsHeight - 50) }, isHorizontal: false) + { + Stretch = true, + RelativeSpacing = panelSpacing + }; + + var panelContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), innerFrame.RectTransform, Anchor.Center), isHorizontal: true) + { + Stretch = true, + RelativeSpacing = panelSpacing + }; + + GUILayoutGroup panelHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 1.0f), panelContainer.RectTransform)) + { + Stretch = true, + RelativeSpacing = panelSpacing + }; + + GUILayoutGroup bottomBar = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), innerFrame.RectTransform)) + { + Stretch = true, + IsHorizontal = true, + RelativeSpacing = panelSpacing + }; + GUILayoutGroup bottomBarLeft = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1.0f), bottomBar.RectTransform)) + { + Stretch = true, + IsHorizontal = true, + RelativeSpacing = panelSpacing + }; + GUILayoutGroup bottomBarMid = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), bottomBar.RectTransform)) + { + Stretch = true, + IsHorizontal = true, + RelativeSpacing = panelSpacing + }; + GUILayoutGroup bottomBarRight = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1.0f), bottomBar.RectTransform)) + { + Stretch = true, + IsHorizontal = true, + RelativeSpacing = panelSpacing + }; + + //server info panel ------------------------------------------------------------ + + infoFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), panelHolder.RectTransform)); + var infoFrameContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), infoFrame.RectTransform, Anchor.Center)) + { + Stretch = true, + RelativeSpacing = 0.025f + }; + + //gamemode tab buttons ------------------------------------------------------------ + + var gameModeTabButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.03f), panelHolder.RectTransform), isHorizontal: true) + { + RelativeSpacing = 0.01f + }; + gameModeViewButton = new GUIButton(new RectTransform(new Vector2(0.25f, 1.4f), gameModeTabButtonContainer.RectTransform), + TextManager.Get("GameMode"), style: "GUITabButton") + { + Selected = true, + OnClicked = (bt, userData) => { ToggleCampaignView(false); return true; } + }; + campaignViewButton = new GUIButton(new RectTransform(new Vector2(0.25f, 1.4f), gameModeTabButtonContainer.RectTransform), + TextManager.Get("CampaignLabel"), style: "GUITabButton") + { + Visible = false, + OnClicked = (bt, userData) => { ToggleCampaignView(true); return true; } + }; + + //server game panel ------------------------------------------------------------ + + modeFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), panelHolder.RectTransform)) + { + CanBeFocused = false + }; + + gameModeContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), modeFrame.RectTransform, Anchor.Center)) + { + RelativeSpacing = panelSpacing * 2.0f, + Stretch = true + }; + + campaignContainer = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.9f), modeFrame.RectTransform, Anchor.Center), style: null) { Visible = false }; - float panelSpacing = 0.02f; + new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), bottomBarLeft.RectTransform), TextManager.Get("disconnect"), style: "GUIButtonLarge") + { + OnClicked = (bt, userdata) => { GameMain.QuitToMainMenu(save: false, showVerificationPrompt: true); return true; } + }; - //server info panel ------------------------------------------------------------ + // file transfers ------------------------------------------------------------ + FileTransferFrame = new GUIFrame(new RectTransform(Vector2.One, bottomBarLeft.RectTransform), style: "TextFrame"); + var fileTransferContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), FileTransferFrame.RectTransform, Anchor.Center)) + { + Stretch = true, + RelativeSpacing = 0.05f + }; + FileTransferTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), fileTransferContent.RectTransform), "", font: GUI.SmallFont); + var fileTransferBottom = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), fileTransferContent.RectTransform), isHorizontal: true) + { + Stretch = true + }; + FileTransferProgressBar = new GUIProgressBar(new RectTransform(new Vector2(0.6f, 1.0f), fileTransferBottom.RectTransform), 0.0f, Color.DarkGreen); + FileTransferProgressText = new GUITextBlock(new RectTransform(Vector2.One, FileTransferProgressBar.RectTransform), "", + font: GUI.SmallFont, textAlignment: Alignment.CenterLeft); + new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), fileTransferBottom.RectTransform), TextManager.Get("cancel")) + { + OnClicked = (btn, userdata) => + { + if (!(userdata is FileReceiver.FileTransferIn transfer)) { return false; } + GameMain.Client?.CancelFileTransfer(transfer); + GameMain.Client.FileReceiver.StopTransfer(transfer); + return true; + } + }; - infoFrame = new GUIFrame(new RectTransform(new Vector2(0.7f, 0.65f), defaultModeContainer.RectTransform)); - var infoFrameContent = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.9f), infoFrame.RectTransform, Anchor.Center), style: null); + // Sidebar area (Character customization/Chat) + + GUILayoutGroup sideBar = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1.0f), panelContainer.RectTransform, maxSize: new Point(650, panelContainer.RectTransform.Rect.Height))) + { + Stretch = true + }; + + //player info panel ------------------------------------------------------------ + + myCharacterFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), sideBar.RectTransform)); + playerInfoContainer = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), myCharacterFrame.RectTransform, Anchor.Center), style: null); + + spectateBox = new GUITickBox(new RectTransform(new Vector2(0.06f, 0.06f), myCharacterFrame.RectTransform) { RelativeOffset = new Vector2(0.05f,0.05f) }, + TextManager.Get("spectatebutton")) + { + Selected = false, + OnSelected = ToggleSpectate, + UserData = "spectate" + }; + + // Social area + + GUIFrame logBackground = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), sideBar.RectTransform)); + GUILayoutGroup logHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), logBackground.RectTransform, Anchor.Center)) + { + Stretch = true + }; + + GUILayoutGroup socialHolder = null; GUILayoutGroup serverLogHolder = null; + + LogButtons = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), logHolder.RectTransform), true) + { + Stretch = true, + RelativeSpacing = 0.02f + }; + + clientHiddenElements.Add(LogButtons); + + // Show chat button + showChatButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.25f), LogButtons.RectTransform), + TextManager.Get("Chat"), style: "GUITabButton") + { + Selected = true, + OnClicked = (GUIButton button, object userData) => + { + if (socialHolder != null) { socialHolder.Visible = true; } + if (serverLogHolder != null) { serverLogHolder.Visible = false; } + showChatButton.Selected = true; + showLogButton.Selected = false; + return true; + } + }; + + // Server log button + showLogButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.25f), LogButtons.RectTransform), + TextManager.Get("ServerLog"), style: "GUITabButton") + { + OnClicked = (GUIButton button, object userData) => + { + if (socialHolder != null) { socialHolder.Visible = false; } + if (!(serverLogHolder?.Visible ?? true)) + { + serverLogHolder.Visible = true; + GameMain.Client.ServerSettings.ServerLog.AssignLogFrame(serverLogBox, serverLogFilterTicks.Content, serverLogFilter); + } + showChatButton.Selected = false; + showLogButton.Selected = true; + return true; + } + }; + + GUITextBlock.AutoScaleAndNormalize(showChatButton.TextBlock, showLogButton.TextBlock); + + GUIFrame logHolderBottom = new GUIFrame(new RectTransform(Vector2.One, logHolder.RectTransform), style: null) + { + CanBeFocused = false + }; + + socialHolder = new GUILayoutGroup(new RectTransform(Vector2.One, logHolderBottom.RectTransform, Anchor.Center)) + { + Stretch = true + }; + + // Spacing + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), socialHolder.RectTransform), style: null) + { + CanBeFocused = false + }; + + GUILayoutGroup socialHolderHorizontal = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), socialHolder.RectTransform), isHorizontal: true) + { + Stretch = true + }; //chatbox ---------------------------------------------------------------------- - chatFrame = new GUIFrame(new RectTransform(new Vector2(0.7f, 0.35f - panelSpacing), defaultModeContainer.RectTransform, Anchor.BottomLeft)); - GUIFrame paddedChatFrame = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.85f), chatFrame.RectTransform, Anchor.Center), style: null); - chatBox = new GUIListBox(new RectTransform(new Point(paddedChatFrame.Rect.Width, paddedChatFrame.Rect.Height - 30), paddedChatFrame.RectTransform) { IsFixedSize = false }); - textBox = new GUITextBox(new RectTransform(new Point(paddedChatFrame.Rect.Width, 20), paddedChatFrame.RectTransform, Anchor.BottomLeft) { IsFixedSize = false }) + chatBox = new GUIListBox(new RectTransform(new Vector2(0.6f, 1.0f), socialHolderHorizontal.RectTransform)); + + //player list ------------------------------------------------------------------ + + playerList = new GUIListBox(new RectTransform(new Vector2(0.4f, 1.0f), socialHolderHorizontal.RectTransform)) + { + OnSelected = (component, userdata) => { SelectPlayer(userdata as Client); return true; } + }; + + // Spacing + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), socialHolder.RectTransform), style: null) + { + CanBeFocused = false + }; + + // Chat input + + var chatRow = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.07f), socialHolder.RectTransform), true) + { + Stretch = true + }; + + chatInput = new GUITextBox(new RectTransform(new Vector2(0.95f, 1.0f), chatRow.RectTransform)) + { + MaxTextLength = ChatMessage.MaxLength, + Font = GUI.SmallFont, + DeselectAfterMessage = false + }; + + micIcon = new GUIImage(new RectTransform(new Vector2(0.05f, 1.0f), chatRow.RectTransform), style: "GUIMicrophoneUnavailable"); + + serverLogHolder = new GUILayoutGroup(new RectTransform(Vector2.One, logHolderBottom.RectTransform, Anchor.Center)) + { + Stretch = true, + Visible = false + }; + + // Spacing + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), serverLogHolder.RectTransform), style: null) + { + CanBeFocused = false + }; + + GUILayoutGroup serverLogHolderHorizontal = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), serverLogHolder.RectTransform), isHorizontal: true) + { + Stretch = true + }; + + //server log ---------------------------------------------------------------------- + + serverLogBox = new GUIListBox(new RectTransform(new Vector2(0.7f, 1.0f), serverLogHolderHorizontal.RectTransform)); + + //filter tickbox list ------------------------------------------------------------------ + + serverLogFilterTicks = new GUIListBox(new RectTransform(new Vector2(0.3f, 1.0f), serverLogHolderHorizontal.RectTransform) { MinSize = new Point(150, 0) }) + { + OnSelected = (component, userdata) => { return false; } + }; + + // Spacing + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), serverLogHolder.RectTransform), style: null) + { + CanBeFocused = false + }; + + // Filter text input + + serverLogFilter = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.07f), serverLogHolder.RectTransform)) { MaxTextLength = ChatMessage.MaxLength, Font = GUI.SmallFont }; - textBox.OnEnterPressed = (tb, userdata) => { GameMain.Client?.EnterChatMessage(tb, userdata); return true; }; - textBox.OnTextChanged += (tb, userdata) => { GameMain.Client?.TypingChatMessage(tb, userdata); return true; }; - - //player info panel ------------------------------------------------------------ - - myCharacterFrame = new GUIFrame(new RectTransform(new Vector2(0.3f - panelSpacing, 0.65f), defaultModeContainer.RectTransform, Anchor.TopRight)); - playerInfoContainer = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), myCharacterFrame.RectTransform, Anchor.Center), style: null); - - playYourself = new GUITickBox(new RectTransform(new Vector2(0.06f, 0.06f), myCharacterFrame.RectTransform) { RelativeOffset = new Vector2(0.05f,0.05f) }, - TextManager.Get("PlayYourself")) + roundControlsHolder = new GUILayoutGroup(new RectTransform(Vector2.One, bottomBarRight.RectTransform), + isHorizontal: true) { - Selected = true, - OnSelected = TogglePlayYourself, - UserData = "playyourself" + Stretch = true }; - - //player list ------------------------------------------------------------------ - playerListFrame = new GUIFrame(new RectTransform(new Vector2(0.3f - panelSpacing, 0.35f - panelSpacing), defaultModeContainer.RectTransform, Anchor.BottomRight)); - GUIFrame paddedPlayerListFrame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.85f), playerListFrame.RectTransform, Anchor.Center), style: null); - - playerList = new GUIListBox(new RectTransform(Vector2.One, paddedPlayerListFrame.RectTransform)) + GUIFrame readyToStartContainer = new GUIFrame(new RectTransform(Vector2.One, roundControlsHolder.RectTransform), style: "TextFrame") { - OnSelected = (component, userdata) => { SelectPlayer(userdata as Client); return true; } + Visible = false }; + // Ready to start tickbox + ReadyToStartBox = new GUITickBox(new RectTransform(new Vector2(0.95f, 0.75f), readyToStartContainer.RectTransform, anchor: Anchor.Center), + TextManager.Get("ReadyToStartTickBox")); + + // Spectate button + spectateButton = new GUIButton(new RectTransform(Vector2.One, roundControlsHolder.RectTransform), + TextManager.Get("SpectateButton"), style: "GUIButtonLarge"); + + // Start button + StartButton = new GUIButton(new RectTransform(Vector2.One, roundControlsHolder.RectTransform), + TextManager.Get("StartGameButton"), style: "GUIButtonLarge") + { + OnClicked = (btn, obj) => + { + GameMain.Client.RequestStartRound(); + CoroutineManager.StartCoroutine(WaitForStartRound(StartButton, allowCancel: true), "WaitForStartRound"); + return true; + } + }; + clientHiddenElements.Add(StartButton); + + //autorestart ------------------------------------------------------------------ + + autoRestartText = new GUITextBlock(new RectTransform(Vector2.One, bottomBarMid.RectTransform), "", font: GUI.SmallFont, style: "TextFrame", textAlignment: Alignment.Center); + GUIFrame autoRestartBoxContainer = new GUIFrame(new RectTransform(Vector2.One, bottomBarMid.RectTransform), style: "TextFrame"); + autoRestartBox = new GUITickBox(new RectTransform(new Vector2(0.95f, 0.75f), autoRestartBoxContainer.RectTransform, Anchor.Center), TextManager.Get("AutoRestart")) + { + OnSelected = (tickBox) => + { + GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, autoRestart: tickBox.Selected); + return true; + } + }; + clientDisabledElements.Add(autoRestartBoxContainer); + //-------------------------------------------------------------------------------------------------------------------------------- //infoframe contents //-------------------------------------------------------------------------------------------------------------------------------- - var infoColumnContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.7f - 0.02f, 0.75f), infoFrameContent.RectTransform, Anchor.BottomLeft), - isHorizontal: true, childAnchor: Anchor.BottomLeft) - { RelativeSpacing = 0.02f, Stretch = true }; - var leftInfoColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.35f, 1.0f), infoColumnContainer.RectTransform, Anchor.BottomLeft)) - { RelativeSpacing = 0.02f, Stretch = true }; - var midInfoColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.35f, 1.0f), infoColumnContainer.RectTransform, Anchor.BottomLeft)) - { RelativeSpacing = 0.02f, Stretch = true }; + //server info ------------------------------------------------------------------ - var rightInfoColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 0.9f), infoFrameContent.RectTransform, Anchor.TopRight)) - { RelativeSpacing = 0.02f, Stretch = true }; - - var topButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.07f), rightInfoColumn.RectTransform), isHorizontal: true, childAnchor: Anchor.TopRight) + // Server Info Header + GUILayoutGroup lobbyHeader = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), infoFrameContent.RectTransform), isHorizontal: true) { - RelativeSpacing = 0.05f, Stretch = true }; - //spacing - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.03f), rightInfoColumn.RectTransform), style: null); - - //server info ------------------------------------------------------------------ - - ServerName = new GUITextBox(new RectTransform(new Vector2(infoColumnContainer.RectTransform.RelativeSize.X, 0.05f), infoFrameContent.RectTransform)) + ServerName = new GUITextBox(new RectTransform(Vector2.One, lobbyHeader.RectTransform)) { MaxTextLength = NetConfig.ServerNameMaxLength, OverflowClip = true @@ -347,7 +671,33 @@ namespace Barotrauma }; clientDisabledElements.Add(ServerName); - var serverMessageContainer = new GUIListBox(new RectTransform(new Vector2(infoColumnContainer.RectTransform.RelativeSize.X, 0.15f), infoFrameContent.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.07f) }); + SettingsButton = new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), lobbyHeader.RectTransform, Anchor.TopRight), + TextManager.Get("ServerSettingsButton"), style: "GUIButtonLarge"); + clientHiddenElements.Add(SettingsButton); + + GUILayoutGroup lobbyContent = new GUILayoutGroup(new RectTransform(Vector2.One, infoFrameContent.RectTransform), isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.025f + }; + + GUILayoutGroup serverInfoHolder = new GUILayoutGroup(new RectTransform(Vector2.One, lobbyContent.RectTransform)) + { + Stretch = true, + RelativeSpacing = 0.025f + }; + + var serverBanner = new GUICustomComponent(new RectTransform(new Vector2(1.0f, 0.25f), serverInfoHolder.RectTransform), DrawServerBanner) + { + HideElementsOutsideFrame = true + }; + new GUITextBlock(new RectTransform(new Vector2(0.15f, 0.05f), serverBanner.RectTransform) { RelativeOffset = new Vector2(0.01f, 0.04f) }, + "", font: GUI.SmallFont, textAlignment: Alignment.Center, textColor: Color.White, style: "GUISlopedHeader") + { + CanBeFocused = false + }; + + var serverMessageContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.75f), serverInfoHolder.RectTransform)); ServerMessage = new GUITextBox(new RectTransform(Vector2.One, serverMessageContainer.Content.RectTransform)) { Wrap = true @@ -365,34 +715,17 @@ namespace Barotrauma GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Message); }; clientDisabledElements.Add(ServerMessage); - - SettingsButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), topButtonContainer.RectTransform, Anchor.TopRight), - TextManager.Get("ServerSettingsButton")); - clientHiddenElements.Add(SettingsButton); - - ShowLogButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), topButtonContainer.RectTransform, Anchor.TopRight), - TextManager.Get("ServerLog")) - { - OnClicked = (GUIButton button, object userData) => - { - if (GameMain.NetworkMember.ServerSettings.ServerLog.LogFrame == null) - { - GameMain.NetworkMember.ServerSettings.ServerLog.CreateLogFrame(); - } - else - { - GameMain.NetworkMember.ServerSettings.ServerLog.LogFrame = null; - GUI.KeyboardDispatcher.Subscriber = null; - } - return true; - } - }; - clientHiddenElements.Add(ShowLogButton); //submarine list ------------------------------------------------------------------ - - var subLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), leftInfoColumn.RectTransform), TextManager.Get("Submarine")); - subList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.9f), leftInfoColumn.RectTransform)) + + GUILayoutGroup subHolder = new GUILayoutGroup(new RectTransform(Vector2.One, lobbyContent.RectTransform)) + { + RelativeSpacing = panelSpacing, + Stretch = true + }; + + var subLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), subHolder.RectTransform) { MinSize = new Point(0, 25) }, TextManager.Get("Submarine")); + subList = new GUIListBox(new RectTransform(Vector2.One, subHolder.RectTransform)) { OnSelected = VotableClicked }; @@ -404,19 +737,40 @@ namespace Barotrauma Visible = false }; - //respawn shuttle ------------------------------------------------------------------ + //respawn shuttle / submarine preview ------------------------------------------------------------------ - shuttleTickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), midInfoColumn.RectTransform), TextManager.Get("RespawnShuttle")) + GUILayoutGroup rightColumn = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), lobbyContent.RectTransform)) { - Selected = true, + RelativeSpacing = panelSpacing, + Stretch = true + }; + + GUILayoutGroup shuttleHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), rightColumn.RectTransform) { MinSize = new Point(0, 25) }, isHorizontal: true) + { + Stretch = true + }; + + shuttleTickBox = new GUITickBox(new RectTransform(Vector2.One, shuttleHolder.RectTransform), TextManager.Get("RespawnShuttle")) + { + Selected = true, OnSelected = (GUITickBox box) => { - shuttleList.Enabled = box.Selected; GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, useRespawnShuttle: box.Selected); return true; } }; - shuttleList = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.05f), midInfoColumn.RectTransform), elementCount: 10) + shuttleTickBox.TextBlock.RectTransform.SizeChanged += () => + { + shuttleTickBox.TextBlock.AutoScale = true; + shuttleTickBox.TextBlock.TextScale = 1.0f; + if (shuttleTickBox.TextBlock.TextScale < 0.75f) + { + shuttleTickBox.TextBlock.Wrap = true; + shuttleTickBox.TextBlock.AutoScale = true; + shuttleTickBox.TextBlock.TextScale = 1.0f; + } + }; + shuttleList = new GUIDropDown(new RectTransform(Vector2.One, shuttleHolder.RectTransform), elementCount: 10) { OnSelected = (component, obj) => { @@ -424,106 +778,205 @@ namespace Barotrauma return true; } }; + shuttleList.ListBox.RectTransform.MinSize = new Point(250, 0); - //gamemode ------------------------------------------------------------------ - - var modeLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), midInfoColumn.RectTransform), TextManager.Get("GameMode")); - modeList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.4f), midInfoColumn.RectTransform)) + subPreviewContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), rightColumn.RectTransform), style: null); + subPreviewContainer.RectTransform.SizeChanged += () => { - OnSelected = VotableClicked + if (SelectedSub != null) + { + subPreviewContainer.ClearChildren(); + SelectedSub.CreatePreviewWindow(subPreviewContainer); + } + }; + + //------------------------------------------------------------------------------------------------------------------ + // Gamemode panel + //------------------------------------------------------------------------------------------------------------------ + + GUILayoutGroup miscSettingsHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.075f), gameModeContainer.RectTransform), isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.01f }; + miscSettingsHolder.RectTransform.SizeChanged += () => + { + miscSettingsHolder.Recalculate(); + foreach (GUIComponent child in miscSettingsHolder.Children) + { + if (child is GUITextBlock textBlock) + { + textBlock.TextScale = 1; + textBlock.AutoScale = true; + textBlock.SetTextPos(); + } + else if (child is GUITickBox tickBox) + { + tickBox.TextBlock.TextScale = 1; + tickBox.TextBlock.AutoScale = true; + tickBox.TextBlock.SetTextPos(); + } + } + }; + + //seed ------------------------------------------------------------------ + + var seedLabel = new GUITextBlock(new RectTransform(Vector2.One, miscSettingsHolder.RectTransform), TextManager.Get("LevelSeed")); + seedLabel.RectTransform.MaxSize = new Point((int)(seedLabel.TextSize.X + 30 * GUI.Scale), int.MaxValue); + SeedBox = new GUITextBox(new RectTransform(new Vector2(0.25f, 1.0f), miscSettingsHolder.RectTransform)); + SeedBox.OnDeselected += (textBox, key) => + { + GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.LevelSeed); + }; + clientDisabledElements.Add(SeedBox); + LevelSeed = ToolBox.RandomSeed(8); + + //level difficulty ------------------------------------------------------------------ + + var difficultyLabel = new GUITextBlock(new RectTransform(Vector2.One, miscSettingsHolder.RectTransform), TextManager.Get("LevelDifficulty")) + { + ToolTip = TextManager.Get("leveldifficultyexplanation") + }; + levelDifficultyScrollBar = new GUIScrollBar(new RectTransform(new Vector2(0.25f, 1.0f), miscSettingsHolder.RectTransform), barSize: 0.2f) + { + Step = 0.05f, + Range = new Vector2(0.0f, 100.0f), + ToolTip = TextManager.Get("leveldifficultyexplanation"), + OnReleased = (scrollbar, value) => + { + GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, levelDifficulty: scrollbar.BarScrollValue); + return true; + } + }; + difficultyLabel.RectTransform.MaxSize = new Point((int)(difficultyLabel.TextSize.X + 30 * GUI.Scale), int.MaxValue); + var difficultyName = new GUITextBlock(new RectTransform(new Vector2(0.25f, 1.0f), miscSettingsHolder.RectTransform), "") + { + ToolTip = TextManager.Get("leveldifficultyexplanation") + }; + levelDifficultyScrollBar.OnMoved = (scrollbar, value) => + { + if (EventManagerSettings.List.Count == 0) { return true; } + difficultyName.Text = EventManagerSettings.List[Math.Min((int)Math.Floor(value * EventManagerSettings.List.Count), EventManagerSettings.List.Count - 1)].Name; + difficultyName.TextColor = Color.Lerp(ToolBox.GradientLerp(scrollbar.BarScroll, Color.LightGreen, Color.Orange, Color.Red), difficultyLabel.TextColor, 0.5f); + return true; + }; + + clientDisabledElements.Add(levelDifficultyScrollBar); + + //gamemode ------------------------------------------------------------------ + + GUILayoutGroup gameModeBackground = new GUILayoutGroup(new RectTransform(Vector2.One, gameModeContainer.RectTransform), isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.01f + }; + + GUILayoutGroup gameModeHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.333f, 1.0f), gameModeBackground.RectTransform)) + { + Stretch = true + }; + + var modeLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), gameModeHolder.RectTransform) { MinSize = new Point(0, 25) }, TextManager.Get("GameMode")); voteText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), modeLabel.RectTransform, Anchor.TopRight), TextManager.Get("Votes"), textAlignment: Alignment.CenterRight) { UserData = "modevotes", Visible = false }; + modeList = new GUIListBox(new RectTransform(Vector2.One, gameModeHolder.RectTransform)) + { + OnSelected = VotableClicked + }; foreach (GameModePreset mode in GameModePreset.List) { - if (mode.IsSinglePlayer) continue; + if (mode.IsSinglePlayer) { continue; } - GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), modeList.Content.RectTransform), + GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), modeList.Content.RectTransform) { MinSize = new Point(0, (int)(30 * GUI.Scale)) }, mode.Name, style: "ListBoxElement", textAlignment: Alignment.CenterLeft) { - UserData = mode + UserData = mode, }; textBlock.ToolTip = mode.Description; } - //mission type ------------------------------------------------------------------ - - missionTypeContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), midInfoColumn.RectTransform), isHorizontal: true) + var gameModeSpecificFrame = new GUIFrame(new RectTransform(new Vector2(0.333f, 1.0f), gameModeBackground.RectTransform), style: null); + CampaignSetupFrame = new GUIFrame(new RectTransform(Vector2.One, gameModeSpecificFrame.RectTransform), style: null) + { + Visible = false + }; + + //mission type ------------------------------------------------------------------ + MissionTypeFrame = new GUIFrame(new RectTransform(Vector2.One, gameModeSpecificFrame.RectTransform), style: null); + + GUILayoutGroup missionHolder = new GUILayoutGroup(new RectTransform(Vector2.One, MissionTypeFrame.RectTransform)) { - UserData = 0, - Visible = false, Stretch = true }; - var missionTypeText = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1.0f), missionTypeContainer.RectTransform), - TextManager.Get("MissionType")); - missionTypeButtons = new GUIButton[2]; - missionTypeButtons[0] = new GUIButton(new RectTransform(new Vector2(0.1f, 1.0f), missionTypeContainer.RectTransform), "<") + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), missionHolder.RectTransform) { MinSize = new Point(0, 25) }, TextManager.Get("MissionType")); + missionTypeList = new GUIListBox(new RectTransform(Vector2.One, missionHolder.RectTransform)) { - OnClicked = (button, obj) => + OnSelected = (component, obj) => { - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, missionType: -1); - - return true; + return false; } }; - new GUITextBlock(new RectTransform(new Vector2(0.4f, 1.0f), missionTypeContainer.RectTransform), - TextManager.Get("MissionType.Random"), textAlignment: Alignment.Center); - missionTypeButtons[1] = new GUIButton(new RectTransform(new Vector2(0.1f, 1.0f), missionTypeContainer.RectTransform), ">") + missionTypeTickBoxes = new GUITickBox[Enum.GetValues(typeof(MissionType)).Length - 2]; + int index = 0; + foreach (MissionType missionType in Enum.GetValues(typeof(MissionType))) { - OnClicked = (button, obj) => + if (missionType == MissionType.None || missionType == MissionType.All) { continue; } + + GUIFrame frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), missionTypeList.Content.RectTransform) { MinSize = new Point(0, (int)(30 * GUI.Scale)) }, style: "ListBoxElement") { - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, missionType: 1); + UserData = index, + }; - return true; - } - }; - - clientDisabledElements.AddRange(missionTypeButtons); - - //seed ------------------------------------------------------------------ - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), rightInfoColumn.RectTransform), TextManager.Get("LevelSeed")); - seedBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.05f), rightInfoColumn.RectTransform)); - seedBox.OnDeselected += (textBox, key) => - { - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.LevelSeed); - }; - clientDisabledElements.Add(seedBox); - LevelSeed = ToolBox.RandomSeed(8); - - //level difficulty ------------------------------------------------------------------ - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), rightInfoColumn.RectTransform), TextManager.Get("LevelDifficulty")); - levelDifficultyScrollBar = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.05f), rightInfoColumn.RectTransform), barSize: 0.1f) - { - Range = new Vector2(0.0f, 100.0f), - OnReleased = (scrollbar, value) => + missionTypeTickBoxes[index] = new GUITickBox(new RectTransform(Vector2.One, frame.RectTransform), + TextManager.Get("MissionType." + missionType.ToString())) { - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, levelDifficulty: scrollbar.BarScrollValue); + UserData = (int)missionType, + OnSelected = (tickbox) => + { + int missionTypeOr = tickbox.Selected ? (int)tickbox.UserData : (int)MissionType.None; + int missionTypeAnd = (int)MissionType.All & (!tickbox.Selected ? (~(int)tickbox.UserData) : (int)MissionType.All); + GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, (int)missionTypeOr, (int)missionTypeAnd); + return true; + } + }; - return true; - } - }; + index++; + } - clientDisabledElements.Add(levelDifficultyScrollBar); + clientDisabledElements.AddRange(missionTypeTickBoxes); //traitor probability ------------------------------------------------------------------ - - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.03f), rightInfoColumn.RectTransform), style: null); //spacing - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), rightInfoColumn.RectTransform), TextManager.Get("Traitors")); + GUILayoutGroup settingsHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.333f, 1.0f), gameModeBackground.RectTransform)) + { + Stretch = true + }; - var traitorProbContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), rightInfoColumn.RectTransform), isHorizontal: true); + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.055f), settingsHolder.RectTransform) { MinSize = new Point(0, 25) }, style: null); + var settingsContent = new GUILayoutGroup(new RectTransform(Vector2.One, settingsHolder.RectTransform)) + { + RelativeSpacing = 0.025f + }; + new GUIFrame(new RectTransform(Vector2.One, settingsContent.RectTransform), style: "InnerFrame") + { + IgnoreLayoutGroups = true + }; + + var traitorsSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), isHorizontal: true) { Stretch = true }; + + new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), traitorsSettingHolder.RectTransform), TextManager.Get("Traitors")); + + var traitorProbContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), traitorsSettingHolder.RectTransform), isHorizontal: true) { Stretch = true }; traitorProbabilityButtons = new GUIButton[2]; - traitorProbabilityButtons[0] = new GUIButton(new RectTransform(new Vector2(0.1f, 1.0f), traitorProbContainer.RectTransform), "<") + traitorProbabilityButtons[0] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), traitorProbContainer.RectTransform), "<") { OnClicked = (button, obj) => { @@ -533,8 +986,8 @@ namespace Barotrauma } }; - traitorProbabilityText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), traitorProbContainer.RectTransform), TextManager.Get("No"), textAlignment: Alignment.Center); - traitorProbabilityButtons[1] = new GUIButton(new RectTransform(new Vector2(0.1f, 1.0f), traitorProbContainer.RectTransform), ">") + traitorProbabilityText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), traitorProbContainer.RectTransform), TextManager.Get("No"), textAlignment: Alignment.Center); + traitorProbabilityButtons[1] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), traitorProbContainer.RectTransform), ">") { OnClicked = (button, obj) => { @@ -547,107 +1000,73 @@ namespace Barotrauma clientDisabledElements.AddRange(traitorProbabilityButtons); //bot count ------------------------------------------------------------------ - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), rightInfoColumn.RectTransform), TextManager.Get("BotCount")); - var botCountContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), rightInfoColumn.RectTransform), isHorizontal: true); + + var botCountSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), isHorizontal: true) { Stretch = true }; + + new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), botCountSettingHolder.RectTransform), TextManager.Get("BotCount")); + var botCountContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), botCountSettingHolder.RectTransform), isHorizontal: true) { Stretch = true }; botCountButtons = new GUIButton[2]; - botCountButtons[0] = new GUIButton(new RectTransform(new Vector2(0.1f, 1.0f), botCountContainer.RectTransform), "<") + botCountButtons[0] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), botCountContainer.RectTransform), "<") { OnClicked = (button, obj) => { GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, botCount: -1); - return true; } }; - botCountText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), botCountContainer.RectTransform), "0", textAlignment: Alignment.Center); - botCountButtons[1] = new GUIButton(new RectTransform(new Vector2(0.1f, 1.0f), botCountContainer.RectTransform), ">") + botCountText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), botCountContainer.RectTransform), "0", textAlignment: Alignment.Center); + botCountButtons[1] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), botCountContainer.RectTransform), ">") { OnClicked = (button, obj) => { GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, botCount: 1); - return true; } }; clientDisabledElements.AddRange(botCountButtons); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), rightInfoColumn.RectTransform), TextManager.Get("BotSpawnMode")); - var botSpawnModeContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), rightInfoColumn.RectTransform), isHorizontal: true); + var botSpawnModeSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), isHorizontal: true) { Stretch = true }; + + new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), botSpawnModeSettingHolder.RectTransform), TextManager.Get("BotSpawnMode")); + var botSpawnModeContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), botSpawnModeSettingHolder.RectTransform), isHorizontal: true) { Stretch = true }; botSpawnModeButtons = new GUIButton[2]; - botSpawnModeButtons[0] = new GUIButton(new RectTransform(new Vector2(0.1f, 1.0f), botSpawnModeContainer.RectTransform), "<") + botSpawnModeButtons[0] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), botSpawnModeContainer.RectTransform), "<") { OnClicked = (button, obj) => { GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, botSpawnMode: -1); - return true; } }; - botSpawnModeText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), botSpawnModeContainer.RectTransform), "", textAlignment: Alignment.Center); - botSpawnModeButtons[1] = new GUIButton(new RectTransform(new Vector2(0.1f, 1.0f), botSpawnModeContainer.RectTransform), ">") + botSpawnModeText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), botSpawnModeContainer.RectTransform), "", textAlignment: Alignment.Center); + botSpawnModeButtons[1] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), botSpawnModeContainer.RectTransform), ">") { OnClicked = (button, obj) => { GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, botSpawnMode: 1); - return true; } }; + List settingsElements = settingsContent.Children.ToList(); + int spacingElementCount = 0; + for (int i = 1; i < settingsElements.Count; i++) + { + settingsElements[i].RectTransform.MinSize = new Point(0, (int)(20 * GUI.Scale)); + if (settingsElements[i] is GUITextBlock) + { + var spacing = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.03f), settingsContent.RectTransform), style: null); + spacing.RectTransform.RepositionChildInHierarchy(i + spacingElementCount); + spacingElementCount++; + } + } + clientDisabledElements.AddRange(botSpawnModeButtons); - - //misc buttons ------------------------------------------------------------------ - - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.03f), rightInfoColumn.RectTransform), style: null); //spacing - - autoRestartBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), rightInfoColumn.RectTransform), TextManager.Get("AutoRestart")) - { - OnSelected = (tickBox) => - { - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, autoRestart: tickBox.Selected); - return true; - } - }; - - clientDisabledElements.Add(autoRestartBox); - var restartText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), rightInfoColumn.RectTransform), "", font: GUI.SmallFont) - { - TextGetter = AutoRestartText - }; - - ReadyToStartBox = new GUITickBox(new RectTransform(new Vector2(0.3f, 0.06f), rightInfoColumn.RectTransform), - TextManager.Get("ReadyToStartTickBox")) - { - 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") - { - OnClicked = (btn, obj) => - { - GameMain.Client.RequestStartRound(); - CoroutineManager.StartCoroutine(WaitForStartRound(StartButton, allowCancel: true), "WaitForStartRound"); - return true; - } - }; - clientHiddenElements.Add(StartButton); - - spectateButton = new GUIButton(new RectTransform(new Vector2(0.3f, 0.1f), infoFrameContent.RectTransform, Anchor.BottomRight), - TextManager.Get("SpectateButton"), style: "GUIButtonLarge"); } - + public IEnumerable WaitForStartRound(GUIButton startButton, bool allowCancel) { string headerText = TextManager.Get("RoundStartingPleaseWait"); @@ -689,30 +1108,33 @@ namespace Barotrauma public override void Deselect() { - textBox.Deselect(); + chatInput.Deselect(); CampaignCharacterDiscarded = false; + HeadSelectionList = null; + JobSelectionFrame = null; + + foreach (Sprite sprite in characterSprites) { sprite.Remove(); } + characterSprites.Clear(); + + foreach (Sprite sprite in jobPreferenceSprites) { sprite.Remove(); } + jobPreferenceSprites.Clear(); } public override void Select() { - if (GameMain.NetworkMember == null) return; + if (GameMain.NetworkMember == null) { return; } + + if (HeadSelectionList != null) { HeadSelectionList.Visible = false; } + if (JobSelectionFrame != null) { JobSelectionFrame.Visible = false; } + Character.Controlled = null; GameMain.LightManager.LosEnabled = false; CampaignCharacterDiscarded = false; - textBox.Select(); - textBox.OnEnterPressed = GameMain.Client.EnterChatMessage; - textBox.OnTextChanged += GameMain.Client.TypingChatMessage; - - subList.Enabled = AllowSubSelection;// || GameMain.Server != null; - shuttleList.Enabled = AllowSubSelection;// || GameMain.Server != null; - - modeList.Enabled = - GameMain.NetworkMember.ServerSettings.Voting.AllowModeVoting || - (GameMain.Client != null && GameMain.Client.HasPermission(ClientPermissions.SelectMode)); - - //ServerName = (GameMain.Server == null) ? ServerName : GameMain.Server.Name; + chatInput.Select(); + chatInput.OnEnterPressed = GameMain.Client.EnterChatMessage; + chatInput.OnTextChanged += GameMain.Client.TypingChatMessage; //disable/hide elements the clients are not supposed to use/see clientDisabledElements.ForEach(c => c.Enabled = false); @@ -723,7 +1145,7 @@ namespace Barotrauma if (GameMain.Client != null) { spectateButton.Visible = GameMain.Client.GameStarted; - ReadyToStartBox.Visible = !GameMain.Client.GameStarted; + ReadyToStartBox.Parent.Visible = !GameMain.Client.GameStarted; ReadyToStartBox.Selected = false; if (campaignUI != null) { @@ -740,60 +1162,10 @@ namespace Barotrauma else { spectateButton.Visible = false; - ReadyToStartBox.Visible = false; + ReadyToStartBox.Parent.Visible = false; } - SetPlayYourself(playYourself.Selected); + SetSpectate(spectateBox.Selected); - /*if (IsServer && GameMain.Server != null) - { - List subsToShow = Submarine.SavedSubmarines.Where(s => !s.HasTag(SubmarineTag.HideInMenus)).ToList(); - - ReadyToStartBox.Visible = false; - StartButton.OnClicked = GameMain.Server.StartGameClicked; - settingsButton.OnClicked = GameMain.Server.ToggleSettingsFrame; - - int prevSelectedSub = subList.SelectedIndex; - UpdateSubList(subList, subsToShow); - - int prevSelectedShuttle = shuttleList.SelectedIndex; - UpdateSubList(shuttleList, subsToShow); - modeList.OnSelected = VotableClicked; - modeList.OnSelected = SelectMode; - subList.OnSelected = VotableClicked; - subList.OnSelected = SelectSub; - shuttleList.OnSelected = SelectSub; - - levelDifficultyScrollBar.OnMoved = (GUIScrollBar scrollBar, float barScroll) => - { - SetLevelDifficulty(barScroll * 100.0f); - return true; - }; - - traitorProbabilityButtons[0].OnClicked = traitorProbabilityButtons[1].OnClicked = ToggleTraitorsEnabled; - botCountButtons[0].OnClicked = botCountButtons[1].OnClicked = ChangeBotCount; - botSpawnModeButtons[0].OnClicked = botSpawnModeButtons[1].OnClicked = ChangeBotSpawnMode; - missionTypeButtons[0].OnClicked = missionTypeButtons[1].OnClicked = ToggleMissionType; - - if (subList.SelectedComponent == null) subList.Select(Math.Max(0, prevSelectedSub)); - if (shuttleList.Selected == null) - { - var shuttles = shuttleList.GetChildren().Where(c => c.UserData is Submarine && ((Submarine)c.UserData).HasTag(SubmarineTag.Shuttle)); - if (prevSelectedShuttle == -1 && shuttles.Any()) - { - shuttleList.SelectItem(shuttles.First().UserData); - } - else - { - shuttleList.Select(Math.Max(0, prevSelectedShuttle)); - } - } - - GameAnalyticsManager.SetCustomDimension01("multiplayer"); - - if (GameModePreset.List.Count > 0 && modeList.SelectedComponent == null) modeList.Select(0); - GameMain.Server.Voting.ResetVotes(GameMain.Server.ConnectedClients); - } - else */ if (GameMain.Client != null) { GameMain.Client.ServerSettings.Voting.ResetVotes(GameMain.Client.ConnectedClients); @@ -801,36 +1173,25 @@ namespace Barotrauma ReadyToStartBox.OnSelected = GameMain.Client.SetReadyToStart; } + roundControlsHolder.Children.ForEach(c => c.IgnoreLayoutGroups = !c.Visible); + roundControlsHolder.Recalculate(); + GameMain.NetworkMember.EndVoteCount = 0; GameMain.NetworkMember.EndVoteMax = 1; base.Select(); } - /*TODO: remove? - public void RandomizeSettings() - { - if (GameMain.Server == null) return; - - if (GameMain.Server.RandomizeSeed) LevelSeed = ToolBox.RandomSeed(8); - if (GameMain.Server.SubSelectionMode == SelectionMode.Random) - { - var nonShuttles = subList.Content.Children.Where(c => c.UserData is Submarine && !((Submarine)c.UserData).HasTag(SubmarineTag.Shuttle)); - subList.Select(nonShuttles.GetRandom()); - } - if (GameMain.Server.ModeSelectionMode == SelectionMode.Random) - { - var allowedGameModes = GameModePreset.List.FindAll(m => !m.IsSinglePlayer && m.Identifier != "multiplayercampaign"); - modeList.Select(allowedGameModes[Rand.Range(0, allowedGameModes.Count)]); - } - }*/ public void UpdatePermissions() { ServerName.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); ServerMessage.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - missionTypeButtons[0].Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - missionTypeButtons[1].Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + missionTypeList.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + foreach (var tickBox in missionTypeTickBoxes) + { + tickBox.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + } traitorProbabilityButtons[0].Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); traitorProbabilityButtons[1].Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); botCountButtons[0].Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); @@ -839,17 +1200,18 @@ namespace Barotrauma botSpawnModeButtons[1].Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); levelDifficultyScrollBar.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); autoRestartBox.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); - seedBox.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); + SeedBox.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); SettingsButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); SettingsButton.OnClicked = GameMain.Client.ServerSettings.ToggleSettingsFrame; - StartButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ManageRound) && !campaignContainer.Visible; + StartButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ManageRound) && !GameMain.Client.GameStarted && !campaignContainer.Visible; ServerName.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); ServerMessage.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); shuttleTickBox.Enabled = GameMain.Client.HasPermission(ClientPermissions.ManageSettings); SubList.Enabled = GameMain.Client.ServerSettings.Voting.AllowSubVoting || GameMain.Client.HasPermission(ClientPermissions.SelectSub); + shuttleList.Enabled = GameMain.Client.HasPermission(ClientPermissions.SelectSub); ModeList.Enabled = GameMain.Client.ServerSettings.Voting.AllowModeVoting || GameMain.Client.HasPermission(ClientPermissions.SelectMode); - ShowLogButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ServerLog); + LogButtons.Visible = GameMain.Client.HasPermission(ClientPermissions.ServerLog); GameMain.Client.ShowLogButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ServerLog); GameMain.Client.EndRoundButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ManageRound); @@ -860,6 +1222,9 @@ namespace Barotrauma (GameMain.Client.HasPermission(ClientPermissions.ManageRound) || GameMain.Client.HasPermission(ClientPermissions.ManageCampaign)); } + + roundControlsHolder.Children.ForEach(c => c.IgnoreLayoutGroups = !c.Visible); + roundControlsHolder.Recalculate(); } public void ShowSpectateButton() @@ -915,18 +1280,19 @@ namespace Barotrauma GameMain.Config.CharacterMoustacheIndex, GameMain.Config.CharacterFaceAttachmentIndex); GameMain.Client.CharacterInfo = characterInfo; + characterInfo.OmitJobInPortraitClothing = true; } parent.ClearChildren(); - GUIComponent infoContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), parent.RectTransform, Anchor.BottomCenter), childAnchor: Anchor.TopCenter) + GUILayoutGroup infoContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), parent.RectTransform, Anchor.BottomCenter), childAnchor: Anchor.TopCenter) { - RelativeSpacing = 0.02f, + RelativeSpacing = 0.015f, Stretch = true, UserData = characterInfo }; - CharacterNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.1f), infoContainer.RectTransform), characterInfo.Name, textAlignment: Alignment.Center) + CharacterNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.065f), infoContainer.RectTransform), characterInfo.Name, textAlignment: Alignment.Center) { MaxTextLength = Client.MaxNameLength, OverflowClip = true @@ -936,6 +1302,7 @@ namespace Barotrauma { if (GameMain.Client == null) { return; } string newName = Client.SanitizeName(tb.Text); + newName = newName.Replace(":", "").Replace(";", ""); if (string.IsNullOrWhiteSpace(newName)) { tb.Text = GameMain.Client.Name; @@ -946,124 +1313,73 @@ namespace Barotrauma GameMain.Client.SetName(tb.Text); }; }; - - GUIComponent headContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.6f, 0.2f), infoContainer.RectTransform, Anchor.TopCenter), isHorizontal: true) - { - Stretch = true - }; - - if (allowEditing) - { - faceSelectionLeft = new GUIButton(new RectTransform(new Vector2(0.1f, 1.0f), headContainer.RectTransform), "", style: "GUIButtonHorizontalArrow") - { - Enabled = generatedHeads.UndoCount > 1, - UserData = -1, - OnClicked = SwitchHead - }; - faceSelectionLeft.Children.ForEach(c => c.SpriteEffects = SpriteEffects.FlipHorizontally); - } - - new GUICustomComponent(new RectTransform(new Vector2(0.3f, 1.0f), headContainer.RectTransform), + + new GUICustomComponent(new RectTransform(new Vector2(0.6f, 0.18f), infoContainer.RectTransform, Anchor.TopCenter), onDraw: (sb, component) => characterInfo.DrawIcon(sb, component.Rect.Center.ToVector2(), targetAreaSize: component.Rect.Size.ToVector2())); - + if (allowEditing) { - faceSelectionRight = new GUIButton(new RectTransform(new Vector2(0.1f, 1.0f), headContainer.RectTransform), style: "GUIButtonHorizontalArrow") - { - UserData = 1, - OnClicked = SwitchHead - }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), infoContainer.RectTransform), - TextManager.Get("Gender"), textAlignment: Alignment.Center); - GUIComponent genderContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 0.06f), infoContainer.RectTransform), isHorizontal: true) + GUILayoutGroup characterInfoTabs = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.04f), infoContainer.RectTransform), isHorizontal: true) { Stretch = true, - RelativeSpacing = 0.05f + RelativeSpacing = 0.02f }; - GUIButton maleButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), genderContainer.RectTransform), - TextManager.Get("Male")) + jobPreferencesButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.33f), characterInfoTabs.RectTransform), + TextManager.Get("JobPreferences"), style: "GUITabButton") { - UserData = Gender.Male, - OnClicked = SwitchGender + Selected = true, + OnClicked = SelectJobPreferencesTab + }; + appearanceButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.33f), characterInfoTabs.RectTransform), + TextManager.Get("CharacterAppearance"), style: "GUITabButton") + { + OnClicked = SelectAppearanceTab }; - GUIButton femaleButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), genderContainer.RectTransform), - TextManager.Get("Female")) + GUITextBlock.AutoScaleAndNormalize(jobPreferencesButton.TextBlock, appearanceButton.TextBlock); + + characterInfoFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), infoContainer.RectTransform), style: null); + + JobList = new GUIListBox(new RectTransform(Vector2.One, characterInfoFrame.RectTransform), true) { - UserData = Gender.Female, - OnClicked = SwitchGender - }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), infoContainer.RectTransform), - TextManager.Get("JobPreferences")); - - jobList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.4f), infoContainer.RectTransform)) - { - Enabled = false - }; - - int i = 1; - foreach (string jobIdentifier in GameMain.Config.JobPreferences) - { - if (!JobPrefab.List.TryGetValue(jobIdentifier, out JobPrefab job)) { continue; } - if (job == null || job.MaxNumber <= 0) continue; - - var jobFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), jobList.Content.RectTransform) { MinSize = new Point(0, 20) }, style: "ListBoxElement") + Enabled = true, + OnSelected = (child, obj) => { - UserData = job - }; - GUITextBlock jobText = new GUITextBlock(new RectTransform(new Vector2(0.66f, 1.0f), jobFrame.RectTransform, Anchor.CenterRight), - i + ". " + job.Name + " ", textAlignment: Alignment.CenterLeft); - - var jobButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 0.8f), jobFrame.RectTransform, Anchor.CenterLeft) { RelativeOffset = new Vector2(0.02f, 0.0f) }, - isHorizontal: true, childAnchor: Anchor.CenterLeft) - { - RelativeSpacing = 0.03f - }; - - int buttonSize = jobButtonContainer.Rect.Height; - GUIButton infoButton = new GUIButton(new RectTransform(new Point(buttonSize, buttonSize), jobButtonContainer.RectTransform), "?") - { - UserData = job, - OnClicked = ViewJobInfo - }; - - GUIButton upButton = new GUIButton(new RectTransform(new Point(buttonSize, buttonSize), jobButtonContainer.RectTransform), "") - { - UserData = -1, - OnClicked = ChangeJobPreference - }; - new GUIImage(new RectTransform(new Vector2(0.8f, 0.8f), upButton.RectTransform, Anchor.Center), GUI.Arrow, scaleToFit: true); - - GUIButton downButton = new GUIButton(new RectTransform(new Point(buttonSize, buttonSize), jobButtonContainer.RectTransform), "") - { - UserData = 1, - OnClicked = ChangeJobPreference - }; - new GUIImage(new RectTransform(new Vector2(0.8f, 0.8f), downButton.RectTransform, Anchor.Center), GUI.Arrow, scaleToFit: true) - { - Rotation = MathHelper.Pi - }; - } - - GUITickBox randPrefTickBox = new GUITickBox( - new RectTransform(new Vector2(0.5f, 0.08f), infoContainer.RectTransform) - { RelativeOffset = new Vector2(-0.0f, 0.0f) }, - TextManager.Get("RandomPreferences")) - { - OnSelected = (tickBox) => - { - if (tickBox.Selected) - { - GameMain.Config.JobPreferences = (new List(GameMain.Config.JobPreferences.Randomize())); - } - return true; + if (child.IsParentOf(GUI.MouseOn)) return false; + return OpenJobSelection(child, obj); } }; - UpdateJobPreferences(jobList); + for (int i = 0; i < 3; i++) + { + Pair jobPrefab = null; + while (i < GameMain.Config.JobPreferences.Count) + { + var jobIdent = GameMain.Config.JobPreferences[i]; + if (!JobPrefab.List.ContainsKey(jobIdent.First)) + { + GameMain.Config.JobPreferences.RemoveAt(i); + continue; + } + jobPrefab = new Pair(JobPrefab.List[jobIdent.First], jobIdent.Second); + break; + } + + var slot = new GUIFrame(new RectTransform(new Vector2(0.333f, 1.0f), JobList.Content.RectTransform), style: "ListBoxElement") + { + CanBeFocused = true, + UserData = jobPrefab + }; + } + + UpdateJobPreferences(JobList); + + appearanceFrame = new GUIFrame(new RectTransform(Vector2.One, characterInfoFrame.RectTransform), style: "GUIFrameListBox") + { + Visible = false, + Color = Color.White + }; } else { @@ -1073,7 +1389,7 @@ namespace Barotrauma foreach (Skill skill in characterInfo.Job.Skills) { Color textColor = Color.White * (0.5f + skill.Level / 200.0f); - var skillText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), infoContainer.RectTransform), + var skillText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.08f), infoContainer.RectTransform), " - " + TextManager.AddPunctuation(':', TextManager.Get("SkillName." + skill.Identifier), ((int)skill.Level).ToString()), textColor); } @@ -1103,44 +1419,31 @@ namespace Barotrauma } } - public bool TogglePlayYourself(GUITickBox tickBox) + public bool ToggleSpectate(GUITickBox tickBox) { - if (tickBox.Selected) - { - UpdatePlayerFrame(campaignCharacterInfo, allowEditing: campaignCharacterInfo == null); - } - else - { - playerInfoContainer.ClearChildren(); - - GameMain.Client.CharacterInfo = null; - GameMain.Client.Character = null; - - new GUITextBlock(new RectTransform(Vector2.One, playerInfoContainer.RectTransform, Anchor.Center), - TextManager.Get("PlayingAsSpectator"), - textAlignment: Alignment.Center); - } + SetSpectate(tickBox.Selected); return false; } - public void SetPlayYourself(bool playYourself) + public void SetSpectate(bool spectate) { - this.playYourself.Selected = playYourself; - if (playYourself) - { - UpdatePlayerFrame(campaignCharacterInfo, allowEditing: campaignCharacterInfo == null); - } - else + this.spectateBox.Selected = spectate; + if (spectate) { playerInfoContainer.ClearChildren(); + GameMain.Client.CharacterInfo?.Remove(); GameMain.Client.CharacterInfo = null; + GameMain.Client.Character?.Remove(); GameMain.Client.Character = null; - new GUITextBlock(new RectTransform(Vector2.One, playerInfoContainer.RectTransform, Anchor.Center), TextManager.Get("PlayingAsSpectator"), textAlignment: Alignment.Center); } + else + { + UpdatePlayerFrame(campaignCharacterInfo, allowEditing: campaignCharacterInfo == null); + } } public void SetAllowSpectating(bool allowSpectating) @@ -1152,13 +1455,12 @@ namespace Barotrauma } //show the player config menu if spectating is not allowed - if (!playYourself.Selected && !allowSpectating) + if (spectateBox.Selected && !allowSpectating) { - playYourself.Selected = !playYourself.Selected; - TogglePlayYourself(playYourself); + spectateBox.Selected = false; } - //hide "play yourself" tickbox if spectating is not allowed - playYourself.Visible = allowSpectating; + //hide spectate tickbox if spectating is not allowed + spectateBox.Visible = allowSpectating; } public void SetAutoRestart(bool enabled, float timer = 0.0f) @@ -1166,13 +1468,10 @@ namespace Barotrauma autoRestartBox.Selected = enabled; autoRestartTimer = timer; } - - public void SetMissionType(int missionTypeIndex) + + public void SetMissionType(MissionType missionType) { - if (missionTypeIndex < 0 || missionTypeIndex >= Enum.GetValues(typeof(MissionType)).Length) return; - - ((GUITextBlock)missionTypeContainer.GetChild(2)).Text = TextManager.Get("MissionType." + ((MissionType)missionTypeIndex).ToString()); - missionTypeContainer.UserData = ((MissionType)missionTypeIndex); + MissionType = missionType; } public void UpdateSubList(GUIComponent subList, List submarines) @@ -1206,7 +1505,7 @@ namespace Barotrauma }; int buttonSize = (int)(frame.Rect.Height * 0.8f); - var subTextBlock = new GUITextBlock(new RectTransform(new Vector2(0.8f, 1.0f), frame.RectTransform, Anchor.CenterLeft) { AbsoluteOffset = new Point(buttonSize + 5, 0) }, + var subTextBlock = new GUITextBlock(new RectTransform(new Vector2(0.8f, 1.0f), frame.RectTransform, Anchor.CenterLeft) /*{ AbsoluteOffset = new Point(buttonSize + 5, 0) }*/, ToolBox.LimitString(sub.DisplayName, GUI.Font, subList.Rect.Width - 65), textAlignment: Alignment.CenterLeft) { CanBeFocused = false @@ -1232,7 +1531,7 @@ namespace Barotrauma subTextBlock.TextColor = new Color(subTextBlock.TextColor, sub.HasTag(SubmarineTag.Shuttle) ? 1.0f : 0.6f); } - GUIButton infoButton = new GUIButton(new RectTransform(new Point(buttonSize, buttonSize), frame.RectTransform, Anchor.CenterLeft) { AbsoluteOffset = new Point((int)(buttonSize * 0.2f), 0) }, "?") + /*GUIButton infoButton = new GUIButton(new RectTransform(new Point(buttonSize, buttonSize), frame.RectTransform, Anchor.CenterLeft) { AbsoluteOffset = new Point((int)(buttonSize * 0.2f), 0) }, "?") { UserData = sub }; @@ -1240,7 +1539,7 @@ namespace Barotrauma { ((Submarine)userdata).CreatePreviewWindow(new GUIMessageBox("", "", new Vector2(0.25f, 0.25f), new Point(500, 400))); return true; - }; + };*/ } if (!sub.RequiredContentPackagesInstalled) @@ -1251,7 +1550,7 @@ namespace Barotrauma if (sub.HasTag(SubmarineTag.Shuttle)) { - new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), frame.RectTransform, Anchor.CenterRight) { RelativeOffset = new Vector2(0.1f, 0.0f) }, + var shuttleText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), frame.RectTransform, Anchor.CenterRight), TextManager.Get("Shuttle", fallBackTag: "RespawnShuttle"), textAlignment: Alignment.CenterRight, font: GUI.SmallFont) { TextColor = subTextBlock.TextColor * 0.8f, @@ -1261,6 +1560,7 @@ namespace Barotrauma //make shuttles more dim in the sub list (selecting a shuttle as the main sub is allowed but not recommended) if (subList == this.subList.Content) { + shuttleText.RectTransform.RelativeOffset = new Vector2(0.1f, 0.0f); subTextBlock.TextColor *= 0.5f; foreach (GUIComponent child in frame.Children) { @@ -1304,6 +1604,11 @@ namespace Barotrauma } return false; } + if (component.UserData is Submarine sub) + { + subPreviewContainer.ClearChildren(); + sub.CreatePreviewWindow(subPreviewContainer); + } voteType = VoteType.Sub; } else if (component.Parent == GameMain.NetLobbyScreen.ModeList.Content) @@ -1312,13 +1617,34 @@ namespace Barotrauma { if (GameMain.Client.HasPermission(ClientPermissions.SelectMode)) { - GameMain.Client.RequestSelectMode(component.Parent.GetChildIndex(component)); string presetName = ((GameModePreset)(component.UserData)).Identifier; + + //display a verification prompt when switching away from the campaign + if (HighlightedModeIndex == SelectedModeIndex && + (GameMain.NetLobbyScreen.ModeList.SelectedData as GameModePreset)?.Identifier == "multiplayercampaign" && + presetName != "multiplayercampaign") + { + var verificationBox = new GUIMessageBox("", TextManager.Get("endcampaignverification"), new string[] { TextManager.Get("yes"), TextManager.Get("no") }); + verificationBox.Buttons[0].OnClicked += (btn, userdata) => + { + GameMain.Client.RequestSelectMode(component.Parent.GetChildIndex(component)); + HighlightMode(SelectedModeIndex); + verificationBox.Close(btn, userdata); + return true; + }; + verificationBox.Buttons[1].OnClicked = verificationBox.Close; + return false; + } + GameMain.Client.RequestSelectMode(component.Parent.GetChildIndex(component)); + HighlightMode(SelectedModeIndex); return (presetName.ToLowerInvariant() != "multiplayercampaign"); } return false; } - else if (!((GameModePreset)userData).Votable) return false; + else if (!((GameModePreset)userData).Votable) + { + return false; + } voteType = VoteType.Mode; } @@ -1334,46 +1660,87 @@ namespace Barotrauma public void AddPlayer(Client client) { - GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), playerList.Content.RectTransform), - client.Name, textAlignment: Alignment.CenterLeft) + GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), playerList.Content.RectTransform) { MinSize = new Point(0, (int)(30 * GUI.Scale)) }, + client.Name, textAlignment: Alignment.CenterLeft, font: GUI.SmallFont, style: null) { + Padding = Vector4.One * 10.0f * GUI.Scale, + Color = Color.White * 0.25f, + HoverColor = Color.White * 0.5f, + SelectedColor = Color.White * 0.85f, + OutlineColor = Color.White * 0.5f, + TextColor = Color.White, UserData = client }; - var soundIcon = new GUIImage(new RectTransform(new Point((int)(textBlock.Rect.Height * 0.8f)), textBlock.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(5, 0) }, + var soundIcon = new GUIImage(new RectTransform(new Point((int)(textBlock.Rect.Height * 0.8f)), textBlock.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(5, 0) }, "GUISoundIcon") { - UserData = "soundicon", + UserData = new Pair("soundicon", 0.0f), CanBeFocused = false, - Visible = true + Visible = true, + OverrideState = GUIComponent.ComponentState.None, + HoverColor = Color.White }; - soundIcon.Color = new Color(soundIcon.Color, 0.0f); + + if (voipSheetRects == null) + { + Point sourceRectSize = soundIcon.Style.Sprites.First().Value.First().Sprite.SourceRect.Size; + var indexPieces = soundIcon.Style.Element.Attribute("sheetindices").Value.Split(';'); + voipSheetRects = new Rectangle[indexPieces.Length]; + for (int i = 0; i < indexPieces.Length; i++) + { + Point location = XMLExtensions.ParsePoint(indexPieces[i].Trim()) * sourceRectSize; + voipSheetRects[i] = new Rectangle(location, sourceRectSize); + } + } + new GUIImage(new RectTransform(new Point((int)(textBlock.Rect.Height * 0.8f)), textBlock.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(5, 0) }, "GUISoundIconDisabled") { UserData = "soundicondisabled", CanBeFocused = true, - Visible = false - }; - new GUITickBox(new RectTransform(new Vector2(0.05f, 0.6f), textBlock.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(10 + soundIcon.Rect.Width, 0) }, "") - { - Selected = true, - Enabled = false, Visible = false, + OverrideState = GUIComponent.ComponentState.None, + HoverColor = Color.White + }; + new GUIFrame(new RectTransform(new Vector2(0.6f, 0.6f), textBlock.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.BothHeight) { AbsoluteOffset = new Point(10 + soundIcon.Rect.Width, 0) }, style: "GUIReadyToStart") + { + Visible = false, + CanBeFocused = false, ToolTip = TextManager.Get("ReadyToStartTickBox"), UserData = "clientready" }; } + public void SetPlayerNameAndJobPreference(Client client) + { + var playerFrame = (GUITextBlock)PlayerList.Content.FindChild(client); + if (playerFrame == null) { return; } + playerFrame.Text = client.Name; + + Color color = Color.White; + if (JobPrefab.List.ContainsKey(client.PreferredJob)) + { + color = JobPrefab.List[client.PreferredJob].UIColor; + } + playerFrame.Color = color * 0.4f; + playerFrame.HoverColor = color * 0.6f; + playerFrame.SelectedColor = color * 0.8f; + playerFrame.OutlineColor = color * 0.5f; + playerFrame.TextColor = color; + } + public void SetPlayerVoiceIconState(Client client, bool muted, bool mutedLocally) { var playerFrame = PlayerList.Content.FindChild(client); if (playerFrame == null) { return; } - var soundIcon = playerFrame.FindChild("soundicon"); + var soundIcon = playerFrame.FindChild(c => c.UserData is Pair pair && pair.First == "soundicon"); var soundIconDisabled = playerFrame.FindChild("soundicondisabled"); + Pair userdata = soundIcon.UserData as Pair; + if (!soundIcon.Visible) { - soundIcon.Color = new Color(soundIcon.Color, 0.0f); + userdata.Second = 0.0f; } soundIcon.Visible = !muted && !mutedLocally; soundIconDisabled.Visible = muted || mutedLocally; @@ -1384,8 +1751,10 @@ namespace Barotrauma { var playerFrame = PlayerList.Content.FindChild(client); if (playerFrame == null) { return; } - var soundIcon = playerFrame.FindChild("soundicon"); - soundIcon.Color = new Color(soundIcon.Color, 1.0f); + var soundIcon = playerFrame.FindChild(c => c.UserData is Pair pair && pair.First == "soundicon"); + Pair userdata = soundIcon.UserData as Pair; + userdata.Second = 0.18f; + soundIcon.Visible = true; } public void RemovePlayer(Client client) @@ -1504,8 +1873,7 @@ namespace Barotrauma //reset rank to custom rankDropDown.SelectItem(null); - var client = playerFrame.UserData as Client; - if (client == null) { return false; } + if (!(playerFrame.UserData is Client client)) { return false; } foreach (GUIComponent child in tickbox.Parent.GetChild().Content.Children) { @@ -1518,7 +1886,7 @@ namespace Barotrauma return true; } }; - var permissionsBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), listBoxContainerLeft.RectTransform)) + var permissionsBox = new GUIListBox(new RectTransform(Vector2.One, listBoxContainerLeft.RectTransform)) { UserData = selectedClient }; @@ -1587,7 +1955,7 @@ namespace Barotrauma return true; } }; - var commandList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), listBoxContainerRight.RectTransform)) + var commandList = new GUIListBox(new RectTransform(Vector2.One, listBoxContainerRight.RectTransform)) { UserData = selectedClient }; @@ -1698,6 +2066,7 @@ namespace Barotrauma private bool ClosePlayerFrame(GUIButton button, object userData) { playerFrame = null; + playerList.Deselect(); return true; } @@ -1722,37 +2091,96 @@ namespace Barotrauma public override void AddToGUIUpdateList() { base.AddToGUIUpdateList(); - - if (campaignContainer.Visible) - { - chatFrame.AddToGUIUpdateList(); - playerListFrame.AddToGUIUpdateList(); - } - + playerFrame?.AddToGUIUpdateList(); - CampaignSetupUI?.AddToGUIUpdateList(); + //CampaignSetupUI?.AddToGUIUpdateList(); jobInfoFrame?.AddToGUIUpdateList(); + + HeadSelectionList?.AddToGUIUpdateList(); + JobSelectionFrame?.AddToGUIUpdateList(); } public override void Update(double deltaTime) { base.Update(deltaTime); - - if (CampaignSetupUI != null) + + string currMicStyle = micIcon.Style.Element.Name.LocalName; + + string targetMicStyle = "GUIMicrophoneEnabled"; + if (GameMain.Config.CaptureDeviceNames == null) { - if (!CampaignSetupUI.Visible) CampaignSetupUI = null; + GameMain.Config.CaptureDeviceNames = OpenAL.Alc.GetStringList(IntPtr.Zero, OpenAL.Alc.CaptureDeviceSpecifier); + } + + if (GameMain.Config.CaptureDeviceNames.Count == 0) + { + targetMicStyle = "GUIMicrophoneUnavailable"; + } + else if (GameMain.Config.VoiceSetting == GameSettings.VoiceMode.Disabled) + { + targetMicStyle = "GUIMicrophoneDisabled"; + } + + if (targetMicStyle.ToLowerInvariant() != currMicStyle.ToLowerInvariant()) + { + GUI.Style.Apply(micIcon, targetMicStyle); } foreach (GUIComponent child in playerList.Content.Children) { - var soundIcon = child.FindChild("soundicon"); - soundIcon.Color = new Color(soundIcon.Color, (soundIcon.Color.A / 255.0f) - (float)deltaTime); + if (child.UserData is Client client) + { + var soundIcon = child.FindChild(c => c.UserData is Pair pair && pair.First == "soundicon"); + if (soundIcon != null) + { + Pair userdata = soundIcon.UserData as Pair; + if (userdata.Second >= 0.0f) + { + userdata.Second = userdata.Second - (float)deltaTime; + + if (userdata.Second < 0.0f) + { + soundIcon.Visible = false; + } + else + { + int sheetIndex = 0; + if (client.ID != GameMain.Client.ID) + { + sheetIndex = (int)Math.Floor((client.VoipSound?.CurrentAmplitude ?? 0.0f) * (voipSheetRects.Length - 0.99f)); + } + else + { + sheetIndex = (int)Math.Floor((VoipCapture.Instance?.LastAmplitude ?? 0.0) * (voipSheetRects.Length - 0.99f)); + } + if (sheetIndex < 0) { sheetIndex = 0; } + if (sheetIndex > voipSheetRects.Length-1) { sheetIndex = voipSheetRects.Length-1; } + soundIcon.sprites.First().Value.First().Sprite.SourceRect = voipSheetRects[sheetIndex]; + } + } + } + } } - if (autoRestartTimer != 0.0f && autoRestartBox.Selected) + autoRestartText.Visible = autoRestartTimer > 0.0f && autoRestartBox.Selected; + if (!MathUtils.NearlyEqual(autoRestartTimer, 0.0f) && autoRestartBox.Selected) { autoRestartTimer = Math.Max(autoRestartTimer - (float)deltaTime, 0.0f); + if (autoRestartTimer > 0.0f) + { + autoRestartText.Text = TextManager.Get("RestartingIn") + " " + ToolBox.SecondsToReadableTime(Math.Max(autoRestartTimer, 0)); + } + } + + if (HeadSelectionList != null && PlayerInput.LeftButtonDown() && !GUI.IsMouseOn(HeadSelectionList)) + { + HeadSelectionList.Visible = false; + } + if (JobSelectionFrame != null && PlayerInput.LeftButtonDown() && !GUI.IsMouseOn(JobSelectionFrame)) + { + JobList.Deselect(); + JobSelectionFrame.Visible = false; } } public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) @@ -1761,109 +2189,571 @@ namespace Barotrauma GUI.DrawBackgroundSprite(spriteBatch, backgroundSprite); - spriteBatch.Begin(SpriteSortMode.Deferred, rasterizerState: GameMain.ScissorTestEnable); - if (campaignUI != null) - { - campaignUI.MapContainer.DrawAuto(spriteBatch); - } + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); + GUI.Draw(Cam, spriteBatch); spriteBatch.End(); } + + private PlayStyle? prevPlayStyle = null; + private void DrawServerBanner(SpriteBatch spriteBatch, GUICustomComponent component) + { + if (GameMain.NetworkMember?.ServerSettings == null) { return; } + + PlayStyle playStyle = GameMain.NetworkMember.ServerSettings.PlayStyle; + if ((int)playStyle < 0 || + (int)playStyle >= GameMain.ServerListScreen.PlayStyleBanners.Length) + { + return; + } + + Sprite sprite = GameMain.ServerListScreen.PlayStyleBanners[(int)playStyle]; + float scale = component.Rect.Width / sprite.size.X; + sprite.Draw(spriteBatch, component.Center, scale: scale); + + if (!prevPlayStyle.HasValue || playStyle != prevPlayStyle.Value) + { + var nameText = component.GetChild(); + nameText.Text = TextManager.Get("servertag." + playStyle); + nameText.Color = GameMain.ServerListScreen.PlayStyleColors[(int)playStyle]; + nameText.RectTransform.NonScaledSize = (nameText.Font.MeasureString(nameText.Text) + new Vector2(25, 10) * GUI.Scale).ToPoint(); + prevPlayStyle = playStyle; + + component.ToolTip = TextManager.Get("servertagdescription." + playStyle); + } + } + public void NewChatMessage(ChatMessage message) { float prevSize = chatBox.BarSize; - while (chatBox.Content.CountChildren > 20) + while (chatBox.Content.CountChildren > 60) { chatBox.RemoveChild(chatBox.Content.Children.First()); } GUITextBlock msg = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), chatBox.Content.RectTransform), - text: (message.Type == ChatMessageType.Private ? TextManager.Get("PrivateMessageTag") + " " : "") + message.TextWithSender, + text: ChatMessage.GetTimeStamp() + (message.Type == ChatMessageType.Private ? TextManager.Get("PrivateMessageTag") + " " : "") + message.TextWithSender, textColor: message.Color, color: ((chatBox.CountChildren % 2) == 0) ? Color.Transparent : Color.Black * 0.1f, wrap: true, font: GUI.SmallFont) { UserData = message, - CanBeFocused = false, + CanBeFocused = false }; + msg.RectTransform.SizeChanged += Recalculate; + void Recalculate() + { + msg.RectTransform.SizeChanged -= Recalculate; + msg.CalculateHeightFromText(); + msg.RectTransform.SizeChanged += Recalculate; + } - if ((prevSize == 1.0f && chatBox.BarScroll == 0.0f) || (prevSize < 1.0f && chatBox.BarScroll == 1.0f)) chatBox.BarScroll = 1.0f; + if ((prevSize == 1.0f && chatBox.BarScroll == 0.0f) || (prevSize < 1.0f && chatBox.BarScroll == 1.0f)) + { + chatBox.BarScroll = 1.0f; + } } - private Memento generatedHeads = new Memento(); - - private bool SwitchHead(GUIButton button, object userData) + private bool SelectJobPreferencesTab(GUIButton button, object userData) { - if (GameMain.Client.CharacterInfo == null) return true; - int dir = (int)userData; + jobPreferencesButton.Selected = true; + appearanceButton.Selected = false; + + JobList.Visible = true; + appearanceFrame.Visible = false; + + return false; + } + + private bool SelectAppearanceTab(GUIButton button, object userData) + { + jobPreferencesButton.Selected = false; + appearanceButton.Selected = true; + + JobList.Visible = false; + appearanceFrame.Visible = true; + + appearanceFrame.ClearChildren(); + if (HeadSelectionList != null) { HeadSelectionList.Visible = false; } + + GUIButton maleButton = null; + GUIButton femaleButton = null; + var info = GameMain.Client.CharacterInfo; - if (!info.HasGenders) + + GUILayoutGroup columnLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), appearanceFrame.RectTransform, Anchor.Center), isHorizontal: true) { - GameMain.Config.CharacterGender = Gender.None; - } - else if (GameMain.Config.CharacterGender == Gender.None) + Stretch = true, + RelativeSpacing = 0.05f + }; + + //left column + GUILayoutGroup leftColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), columnLayout.RectTransform)) { - GameMain.Config.CharacterGender = info.Gender; - } - if (generatedHeads.Current == null) + RelativeSpacing = 0.05f + }; + + GUILayoutGroup genderContainer = new GUILayoutGroup(new RectTransform(new Vector2(2.0f, 0.2f), leftColumn.RectTransform), isHorizontal: true) { - // Add the current head in the memory - generatedHeads.Store(info.Head); - } - if (dir == 1) + Stretch = true, + RelativeSpacing = 0.05f + }; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), genderContainer.RectTransform), TextManager.Get("Gender")); + maleButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), genderContainer.RectTransform), + TextManager.Get("Male"), style: "ListBoxElement") { - // Try redo, if not possible, generate new - var previousHead = generatedHeads.Redo(); - if (previousHead == info.Head || previousHead == null) + UserData = Gender.Male, + OnClicked = OpenHeadSelection, + Selected = info.Gender == Gender.Male + }; + femaleButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), genderContainer.RectTransform), + TextManager.Get("Female"), style: "ListBoxElement") + { + UserData = Gender.Female, + OnClicked = OpenHeadSelection, + Selected = info.Gender == Gender.Female + }; + + int hairCount = info.FilterByTypeAndHeadID(info.FilterElementsByGenderAndRace(info.Wearables), WearableType.Hair).Count(); + if (hairCount > 0) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), leftColumn.RectTransform), TextManager.Get("FaceAttachment.Hair")); + var hairSlider = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.15f), leftColumn.RectTransform)) { - // Generate new and add to the list - // If the head id is the same, regenerate until it's not - // The counter is there to prevent stack overflow if we for some reason cannot get unique ids (e.g. an issue with the head id range or simply if there is no heads defined). - int newHeadId = previousHead.HeadSpriteId; - int counter = 0; - while (newHeadId == previousHead.HeadSpriteId && counter < 10) + Range = new Vector2(0, hairCount), + StepValue = 1, + BarScrollValue = info.HairIndex, + OnMoved = SwitchHair, + BarSize = 1.0f / (float)(hairCount + 1) + }; + } + + int beardCount = info.FilterByTypeAndHeadID(info.FilterElementsByGenderAndRace(info.Wearables), WearableType.Beard).Count(); + if (beardCount > 0) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), leftColumn.RectTransform), TextManager.Get("FaceAttachment.Beard")); + var beardSlider = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.15f), leftColumn.RectTransform)) + { + Range = new Vector2(0, beardCount), + StepValue = 1, + BarScrollValue = info.BeardIndex, + OnMoved = SwitchBeard, + BarSize = 1.0f / (float)(beardCount + 1) + }; + } + + //right column + GUILayoutGroup rightColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), columnLayout.RectTransform)) + { + RelativeSpacing = 0.05f + }; + + //spacing to account for the gender selection in the left column + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), rightColumn.RectTransform), style: null) + { + CanBeFocused = false + }; + + int moustacheCount = info.FilterByTypeAndHeadID(info.FilterElementsByGenderAndRace(info.Wearables), WearableType.Moustache).Count(); + if (moustacheCount > 0) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), rightColumn.RectTransform), TextManager.Get("FaceAttachment.Moustache")); + var moustacheSlider = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.15f), rightColumn.RectTransform)) + { + Range = new Vector2(0, moustacheCount), + StepValue = 1, + BarScrollValue = info.MoustacheIndex, + OnMoved = SwitchMoustache, + BarSize = 1.0f / (float)(moustacheCount + 1) + }; + } + + int faceAttachmentCount = info.FilterByTypeAndHeadID(info.FilterElementsByGenderAndRace(info.Wearables), WearableType.FaceAttachment).Count(); + if (faceAttachmentCount > 0) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), rightColumn.RectTransform), TextManager.Get("FaceAttachment.Accessories")); + var faceAttachmentSlider = new GUIScrollBar(new RectTransform(new Vector2(1.0f, 0.15f), rightColumn.RectTransform)) + { + Range = new Vector2(0, faceAttachmentCount), + StepValue = 1, + BarScrollValue = info.FaceAttachmentIndex, + OnMoved = SwitchFaceAttachment, + BarSize = 1.0f / (float)(faceAttachmentCount + 1) + }; + } + + return false; + } + + private bool OpenHeadSelection(GUIButton button, object userData) + { + Gender selectedGender = (Gender)userData; + if (HeadSelectionList != null) + { + HeadSelectionList.Visible = true; + foreach (GUIComponent child in HeadSelectionList.Content.Children) + { + child.Visible = (Gender)child.UserData == selectedGender; + child.Children.ForEach(c => c.Visible = ((Tuple)c.UserData).Item1 == selectedGender); + } + return true; + } + + var info = GameMain.Client.CharacterInfo; + + HeadSelectionList = new GUIListBox( + new RectTransform(new Point(characterInfoFrame.Rect.Width, (characterInfoFrame.Rect.Bottom - button.Rect.Bottom) + characterInfoFrame.Rect.Height * 2), GUI.Canvas) + { + AbsoluteOffset = new Point(characterInfoFrame.Rect.Right - characterInfoFrame.Rect.Width, button.Rect.Bottom) + }); + + new GUIFrame(new RectTransform(new Vector2(1.25f, 1.25f), HeadSelectionList.RectTransform, Anchor.Center), style: "OuterGlow", color: Color.Black) + { + UserData = "outerglow", + CanBeFocused = false + }; + + GUILayoutGroup row = null; + int itemsInRow = 0; + + XElement headElement = info.Ragdoll.MainElement.Elements().FirstOrDefault(e => e.GetAttributeString("type", "").ToLowerInvariant() == "head"); + XElement headSpriteElement = headElement.Element("sprite"); + string spritePathWithTags = headSpriteElement.Attribute("texture").Value; + + var characterConfigElement = info.CharacterConfigElement; + + var heads = info.Heads; + if (heads != null) + { + row = null; + itemsInRow = 0; + foreach (var head in heads) + { + var headPreset = head.Key; + Gender gender = headPreset.Gender; + Race race = headPreset.Race; + int headIndex = headPreset.ID; + + string spritePath = spritePathWithTags + .Replace("[GENDER]", gender.ToString().ToLowerInvariant()) + .Replace("[RACE]", race.ToString().ToLowerInvariant()); + + if (!File.Exists(spritePath)) { continue; } + + Sprite headSprite = new Sprite(headSpriteElement, "", spritePath); + headSprite.SourceRect = new Rectangle(CharacterInfo.CalculateOffset(headSprite, head.Value.ToPoint()), headSprite.SourceRect.Size); + characterSprites.Add(headSprite); + + if (row == null || itemsInRow >= 4) { - newHeadId = info.GetRandomHeadID(); - counter++; + row = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.333f), HeadSelectionList.Content.RectTransform), true) + { + UserData = gender, + Visible = gender == selectedGender + }; + itemsInRow = 0; } - info.Head = new CharacterInfo.HeadInfo(newHeadId) { gender = GameMain.Config.CharacterGender }; - generatedHeads.Store(info.Head); + + var btn = new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), row.RectTransform), style: "ListBoxElement") + { + OutlineColor = Color.White * 0.5f, + PressedColor = Color.White * 0.5f, + UserData = new Tuple(gender, race, headIndex), + OnClicked = SwitchHead, + Selected = gender == info.Gender && race == info.Race && headIndex == info.HeadSpriteId, + Visible = gender == selectedGender + }; + + new GUIImage(new RectTransform(Vector2.One, btn.RectTransform), headSprite, scaleToFit: true); + itemsInRow++; + } + } + + return false; + } + + private bool SwitchJob(GUIButton button, object obj) + { + int childIndex = JobList.SelectedIndex; + var child = JobList.SelectedComponent; + + bool moveToNext = obj != null; + + var jobPrefab = (obj as Pair)?.First; + + var prevObj = child.UserData; + + var existingChild = JobList.Content.FindChild(d => (d.UserData is Pair prefab) && (prefab.First == jobPrefab)); + if (existingChild != null && obj != null) + { + existingChild.UserData = prevObj; + } + child.UserData = obj; + + for (int i = 0; i < 2; i++) + { + if (i < 2 && JobList.Content.GetChild(i).UserData == null) + { + JobList.Content.GetChild(i).UserData = JobList.Content.GetChild(i + 1).UserData; + JobList.Content.GetChild(i + 1).UserData = null; + } + } + + UpdateJobPreferences(JobList); + + if (moveToNext) + { + var emptyChild = JobList.Content.FindChild(c => c.UserData == null && c.CanBeFocused); + if (emptyChild != null) + { + JobList.Select(JobList.Content.GetChildIndex(emptyChild)); } else { - info.Head = previousHead; + JobList.Deselect(); + if (JobSelectionFrame != null) { JobSelectionFrame.Visible = false; } } } else { - // Undo, if not possible, the button should be disabled - var previousHead = generatedHeads.Undo(); - if (previousHead != info.Head && previousHead != null) + OpenJobSelection(child, child.UserData); + } + + return false; + } + + private bool OpenJobSelection(GUIComponent child, object userData) + { + if (JobSelectionFrame != null) + { + JobSelectionFrame.Visible = true; + return true; + } + + Point frameSize = new Point(characterInfoFrame.Rect.Width, characterInfoFrame.Rect.Height * 2); + JobSelectionFrame = new GUIFrame(new RectTransform(frameSize, GUI.Canvas, Anchor.TopLeft) + { AbsoluteOffset = new Point(characterInfoFrame.Rect.Right - frameSize.X, characterInfoFrame.Rect.Bottom) }, "GUIFrameListBox"); + + new GUIFrame(new RectTransform(new Vector2(1.25f, 1.25f), JobSelectionFrame.RectTransform, Anchor.Center), style: "OuterGlow", color: Color.Black) + { + UserData = "outerglow", + CanBeFocused = false + }; + + var rows = new GUILayoutGroup(new RectTransform(Vector2.One, JobSelectionFrame.RectTransform)) { Stretch = true }; + var row = new GUILayoutGroup(new RectTransform(Vector2.One, rows.RectTransform), true); + + GUIButton jobButton = null; + + var availableJobs = JobPrefab.List.Values.Where(jobPrefab => + jobPrefab.MaxNumber > 0 && JobList.Content.Children.All(c => !(c.UserData is Pair prefab) || prefab.First != jobPrefab) + ).Select(j => new Pair(j, 1)); + availableJobs = availableJobs.Concat( + JobPrefab.List.Values.Where(jobPrefab => + jobPrefab.MaxNumber > 0 && JobList.Content.Children.Any(c => (c.UserData is Pair prefab) && prefab.First == jobPrefab) + ).Select(j => JobList.Content.FindChild(c => (c.UserData is Pair prefab) && prefab.First == j).UserData as Pair)); + availableJobs = availableJobs.ToList(); + + int itemsInRow = 1; + + foreach (var jobPrefab in availableJobs) + { + if (itemsInRow >= 4) { - info.Head = previousHead; + row = new GUILayoutGroup(new RectTransform(Vector2.One, rows.RectTransform), true); + itemsInRow = 0; + } + + jobButton = new GUIButton(new RectTransform(new Vector2(1.0f / 3.0f, 1.0f), row.RectTransform), style: "ListBoxElement") + { + PressedColor = Color.White, + OutlineColor = Color.White * 0.5f, + UserData = jobPrefab, + OnClicked = (btn, usdt) => + { + if (btn.IsParentOf(GUI.MouseOn)) return false; + return SwitchJob(btn, usdt); + } + }; + itemsInRow++; + + var images = AddJobSpritesToGUIComponent(jobButton, jobPrefab.First); + for (int variantIndex = 0; variantIndex < images.Length; variantIndex++) + { + foreach (GUIImage image in images[variantIndex]) + { + characterSprites.Add(image.Sprite); + } + } + + if (images != null && images.Length > 1) + { + jobPrefab.Second = Math.Min(jobPrefab.Second, images.Length); + int currVisible = jobPrefab.Second; + GUIButton currSelected = null; + for (int variantIndex = 0; variantIndex < images.Length; variantIndex++) + { + foreach (GUIImage image in images[variantIndex]) + { + image.Visible = currVisible == (variantIndex + 1); + } + + var variantButton = new GUIButton(new RectTransform(new Vector2(0.15f), jobButton.RectTransform, scaleBasis: ScaleBasis.BothWidth) { RelativeOffset = new Vector2(0.05f, 0.05f + 0.2f * variantIndex) }, (variantIndex + 1).ToString(), style: null) + { + Color = new Color(50, 50, 50, 200), + HoverColor = Color.Gray * 0.75f, + PressedColor = Color.Black * 0.75f, + SelectedColor = new Color(45, 70, 100, 200), + UserData = new Pair(jobPrefab.First, variantIndex+1), + OnClicked = (btn, obj) => + { + currSelected.Selected = false; + int k = ((Pair)obj).Second; + btn.Parent.UserData = obj; + for (int j = 0; j < images.Length; j++) + { + foreach (GUIImage image in images[j]) + { + image.Visible = k == (j + 1); + } + } + currSelected = btn; + currSelected.Selected = true; + + return false; + } + }; + + if (currVisible == (variantIndex + 1)) + { + currSelected = variantButton; + } + } + if (currSelected != null) + { + currSelected.Selected = true; + } } } - info.ReloadHeadAttachments(); - StoreHead(); - GameMain.Config.SaveNewPlayerConfig(); - faceSelectionLeft.Enabled = generatedHeads.UndoCount > 0; + return true; } - private bool SwitchGender(GUIButton button, object obj) + private GUIImage[][] AddJobSpritesToGUIComponent(GUIComponent parent, JobPrefab jobPrefab) { - generatedHeads.Clear(); - Gender gender = (Gender)obj; + GUIFrame innerFrame = null; + List outfitPreviews = jobPrefab.GetJobOutfitSprites(Gender.Male, out Vector2 dimensions); + innerFrame = new GUIFrame(new RectTransform(Vector2.One * 0.8f, parent.RectTransform, Anchor.Center) { RelativeOffset = new Vector2(-0.07f, -0.06f) }, style: null) + { + CanBeFocused = false + }; + + void recalculateInnerFrame() + { + float buttonWidth = parent.Rect.Width; + float buttonHeight = parent.Rect.Height; + + Vector2 innerFrameSize; + if (buttonWidth / dimensions.X > buttonHeight / dimensions.Y) + { + innerFrameSize = new Vector2((dimensions.X / dimensions.Y) * (buttonHeight / buttonWidth), 1.0f); + } + else + { + innerFrameSize = new Vector2(1.0f, (dimensions.Y / dimensions.X) * (buttonWidth / buttonHeight)); + } + + innerFrame.RectTransform.RelativeSize = innerFrameSize * 0.8f; + } + + GUIImage[][] retVal = new GUIImage[0][]; + if (outfitPreviews != null && outfitPreviews.Any()) + { + parent.RectTransform.SizeChanged += recalculateInnerFrame; + + retVal = new GUIImage[outfitPreviews.Count][]; + for (int i = 0; i < outfitPreviews.Count; i++) + { + JobPrefab.OutfitPreview outfitPreview = outfitPreviews[i]; + retVal[i] = new GUIImage[outfitPreview.Sprites.Count]; + for (int j = 0; j < outfitPreview.Sprites.Count; j++) + { + Pair sprite = outfitPreview.Sprites[j]; + retVal[i][j] = new GUIImage(new RectTransform(sprite.First.SourceRect.Size.ToVector2() / dimensions, innerFrame.RectTransform, Anchor.Center) { RelativeOffset = sprite.Second / dimensions }, sprite.First, scaleToFit: true) + { + PressedColor = Color.White, + CanBeFocused = false + }; + } + } + + recalculateInnerFrame(); + } + + var textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), parent.RectTransform, Anchor.BottomCenter), jobPrefab.Name, textAlignment: Alignment.Center) + { + TextColor = jobPrefab.UIColor, + CanBeFocused = false, + AutoScale = true + }; + textBlock.RectTransform.SizeChanged += () => { textBlock.TextScale = 1.0f; }; + + return retVal; + } + + private bool SwitchHead(GUIButton button, object obj) + { var info = GameMain.Client.CharacterInfo; - info.Gender = gender; - info.SetRandomHead(); - info.LoadHeadAttachments(); + Gender gender = ((Tuple)obj).Item1; + Race race = ((Tuple)obj).Item2; + int id = ((Tuple)obj).Item3; + + if (gender != info.Gender || race != info.Race || id != info.HeadSpriteId) + { + info.Head = new CharacterInfo.HeadInfo(id, gender, race); + info.ReloadHeadAttachments(); + } + StoreHead(); + + UpdateJobPreferences(JobList); + + SelectAppearanceTab(button, obj); + + return true; + } + + private bool SwitchHair(GUIScrollBar scrollBar, float barScroll) => SwitchAttachment(scrollBar, WearableType.Hair); + private bool SwitchBeard(GUIScrollBar scrollBar, float barScroll) => SwitchAttachment(scrollBar, WearableType.Beard); + private bool SwitchMoustache(GUIScrollBar scrollBar, float barScroll) => SwitchAttachment(scrollBar, WearableType.Moustache); + private bool SwitchFaceAttachment(GUIScrollBar scrollBar, float barScroll) => SwitchAttachment(scrollBar, WearableType.FaceAttachment); + private bool SwitchAttachment(GUIScrollBar scrollBar, WearableType type) + { + var info = GameMain.Client.CharacterInfo; + int index = (int)scrollBar.BarScrollValue; + switch (type) + { + case WearableType.Beard: + info.Head = new CharacterInfo.HeadInfo(info.HeadSpriteId, info.Gender, info.Race, info.HairIndex, index, info.MoustacheIndex, info.FaceAttachmentIndex); + break; + case WearableType.FaceAttachment: + info.Head = new CharacterInfo.HeadInfo(info.HeadSpriteId, info.Gender, info.Race, info.HairIndex, info.BeardIndex, info.MoustacheIndex, index); + break; + case WearableType.Hair: + info.Head = new CharacterInfo.HeadInfo(info.HeadSpriteId, info.Gender, info.Race, index, info.BeardIndex, info.MoustacheIndex, info.FaceAttachmentIndex); + break; + case WearableType.Moustache: + info.Head = new CharacterInfo.HeadInfo(info.HeadSpriteId, info.Gender, info.Race, info.HairIndex, info.BeardIndex, index, info.FaceAttachmentIndex); + break; + default: + DebugConsole.ThrowError($"Wearable type not implemented: {type.ToString()}"); + return false; + } + info.ReloadHeadAttachments(); StoreHead(); - GameMain.Config.SaveNewPlayerConfig(); return true; } @@ -1890,42 +2780,29 @@ namespace Barotrauma ToggleCampaignMode(false); } - if (modeList.SelectedIndex != modeIndex) { modeList.Select(modeIndex, true); } - - missionTypeContainer.Visible = SelectedMode != null && SelectedMode.Identifier == "mission"; + if ((HighlightedModeIndex == selectedModeIndex || HighlightedModeIndex<0) && modeList.SelectedIndex != modeIndex) { modeList.Select(modeIndex, true); } + selectedModeIndex = modeIndex; + + MissionTypeFrame.Visible = SelectedMode != null && SelectedMode.Identifier == "mission" && HighlightedModeIndex == SelectedModeIndex; + CampaignSetupFrame.Visible = false; } - private bool SelectMode(GUIComponent component, object obj) + public void HighlightMode(int modeIndex) { - if (GameMain.NetworkMember == null || obj == modeList.SelectedData) return false; - - GameModePreset modePreset = obj as GameModePreset; - if (modePreset == null) return false; - - missionTypeContainer.Visible = modePreset.Identifier == "mission"; - if (modePreset.Identifier == "multiplayercampaign") - { - //campaign selected and the campaign view has not been set up yet - // -> don't select the mode yet and start campaign setup - /*if (GameMain.Server != null && !campaignContainer.Visible) - { - campaignSetupUI = MultiPlayerCampaign.StartCampaignSetup(); - return false; - }*/ - } - else - { - ToggleCampaignMode(false); - } + if (modeIndex < 0 || modeIndex >= modeList.Content.CountChildren) { return; } - //lastUpdateID++; - return true; + HighlightedModeIndex = modeIndex; + MissionTypeFrame.Visible = SelectedMode != null && SelectedMode.Identifier == "mission" && HighlightedModeIndex == SelectedModeIndex; + CampaignSetupFrame.Visible = SelectedMode != null && SelectedMode.Identifier == "multiplayercampaign"; } public void ToggleCampaignView(bool enabled) { campaignContainer.Visible = enabled; - defaultModeContainer.Visible = !enabled; + gameModeContainer.Visible = !enabled; + + campaignViewButton.Selected = enabled; + gameModeViewButton.Selected = !enabled; } public void ToggleCampaignMode(bool enabled) @@ -1940,10 +2817,10 @@ namespace Barotrauma } subList.Enabled = !enabled && AllowSubSelection; - shuttleList.Enabled = !enabled && AllowSubSelection; - StartButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ManageRound) && !enabled; + shuttleList.Enabled = !enabled && GameMain.Client.HasPermission(ClientPermissions.SelectSub); + StartButton.Visible = GameMain.Client.HasPermission(ClientPermissions.ManageRound) && GameMain.Client.GameStarted && !enabled; - if (campaignViewButton != null) campaignViewButton.Visible = enabled; + if (campaignViewButton != null) { campaignViewButton.Visible = enabled; } if (enabled) { @@ -1959,16 +2836,14 @@ namespace Barotrauma CoroutineManager.StartCoroutine(WaitForStartRound(campaignUI.StartButton, allowCancel: true), "WaitForStartRound"); } }; - campaignUI.MapContainer.RectTransform.NonScaledSize = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); - var backButton = new GUIButton(new RectTransform(new Vector2(0.2f, 0.08f), campaignContainer.RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.02f) }, - TextManager.Get("Back"), style: "GUIButtonLarge"); - backButton.OnClicked += (btn, obj) => { ToggleCampaignView(false); return true; }; - - var restartText = new GUITextBlock(new RectTransform(new Vector2(0.25f, 0.1f), campaignContainer.RectTransform, Anchor.BottomRight), "", font: GUI.SmallFont) + var campaignMenuContainer = new GUIFrame(new RectTransform(new Vector2(0.4f, 1.0f), campaignContainer.RectTransform, Anchor.TopRight), style: null) { - TextGetter = AutoRestartText + Color = Color.Black }; + CampaignUI.SetMenuPanelParent(campaignMenuContainer.RectTransform); + CampaignUI.SetMissionPanelParent(campaignMenuContainer.RectTransform); + GameMain.GameSession.Map.CenterOffset = new Vector2(-campaignContainer.Rect.Width / 5, 0); } modeList.Select(2, true); } @@ -1982,11 +2857,41 @@ namespace Barotrauma lastUpdateID++; }*/ } - + + public void TryDisplayCampaignSubmarine(Submarine submarine) + { + string name = submarine?.Name; + bool displayed = false; + subList.OnSelected -= VotableClicked; + subList.Deselect(); + subPreviewContainer.ClearChildren(); + foreach (GUIComponent child in subList.Content.Children) + { + Submarine sub = child.UserData as Submarine; + if (sub == null) { continue; } + //just check the name, even though the campaign sub may not be the exact same version + //we're selecting the sub just for show, the selection is not actually used for anything + if (sub.Name == name) + { + subList.Select(sub); + if (Submarine.SavedSubmarines.Contains(sub)) + { + sub.CreatePreviewWindow(subPreviewContainer); + displayed = true; + } + break; + } + } + subList.OnSelected += VotableClicked; + if (!displayed) + { + submarine.CreatePreviewWindow(subPreviewContainer); + } + } + private bool ViewJobInfo(GUIButton button, object obj) { - JobPrefab jobPrefab = button.UserData as JobPrefab; - if (jobPrefab == null) return false; + if (!(button.UserData is JobPrefab jobPrefab)) { return false; } jobInfoFrame = jobPrefab.CreateInfoFrame(); GUIButton closeButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.05f), jobInfoFrame.GetChild(2).GetChild(0).RectTransform, Anchor.BottomRight), @@ -2005,42 +2910,101 @@ namespace Barotrauma return true; } - private bool ChangeJobPreference(GUIButton button, object obj) - { - GUIComponent jobText = button.Parent.Parent; - - int index = jobList.Content.GetChildIndex(jobText); - int newIndex = index + (int)obj; - if (newIndex < 0 || newIndex > jobList.Content.CountChildren - 1) return false; - - jobText.RectTransform.RepositionChildInHierarchy(newIndex); - - UpdateJobPreferences(jobList); - - return true; - } - private void UpdateJobPreferences(GUIListBox listBox) { - listBox.Deselect(); - List jobNamePreferences = new List(); + foreach (Sprite sprite in jobPreferenceSprites) { sprite.Remove(); } + jobPreferenceSprites.Clear(); + List> jobNamePreferences = new List>(); + + bool disableNext = false; for (int i = 0; i < listBox.Content.CountChildren; i++) { - float a = (float)(i - 1) / 3.0f; - a = Math.Min(a, 3); - Color color = new Color(1.0f - a, (1.0f - a) * 0.6f, 0.0f, 0.3f); + GUIComponent slot = listBox.Content.GetChild(i); - GUIComponent child = listBox.Content.GetChild(i); + slot.OutlineColor = Color.White * 0.4f; + slot.Color = Color.Gray; + slot.HoverColor = Color.White; + slot.SelectedColor = Color.White; + + slot.ClearChildren(); - child.Color = color; - child.HoverColor = color; - child.SelectedColor = color; + slot.CanBeFocused = !disableNext; + if (slot.UserData is Pair jobPrefab) + { + var images = AddJobSpritesToGUIComponent(slot, jobPrefab.First); + for (int variantIndex = 0; variantIndex < images.Length; variantIndex++) + { + foreach (GUIImage image in images[variantIndex]) + { + jobPreferenceSprites.Add(image.Sprite); + int selectedVariantIndex = Math.Min(jobPrefab.Second, images.Length); + image.Visible = images.Length == 1 || selectedVariantIndex == (variantIndex + 1); + } + if (images.Length > 1) + { + var variantButton = new GUIButton(new RectTransform(new Vector2(0.15f), slot.RectTransform, scaleBasis: ScaleBasis.BothWidth) { RelativeOffset = new Vector2(0.05f, 0.25f + 0.2f * variantIndex) }, (variantIndex + 1).ToString(), style: null) + { + Color = new Color(50, 50, 50, 200), + HoverColor = Color.Gray * 0.75f, + PressedColor = Color.Black * 0.75f, + SelectedColor = new Color(45, 70, 100, 200), + Selected = jobPrefab.Second == (variantIndex + 1), + UserData = new Pair(jobPrefab.First, variantIndex + 1), + OnClicked = (btn, obj) => + { + int k = ((Pair)obj).Second; + btn.Parent.UserData = obj; + UpdateJobPreferences(listBox); + return false; + } + }; + } + } - (child.GetChild()).Text = (i + 1) + ". " + (child.UserData as JobPrefab).Name; + //info button + new GUIButton(new RectTransform(new Vector2(0.15f), slot.RectTransform, Anchor.TopLeft, scaleBasis: ScaleBasis.BothWidth) { RelativeOffset = new Vector2(0.05f) }, style: "GUIButtonInfo") + { + UserData = jobPrefab.First, + OnClicked = ViewJobInfo + }; - jobNamePreferences.Add((child.UserData as JobPrefab).Identifier); + //remove button + new GUIButton(new RectTransform(new Vector2(0.15f), slot.RectTransform, Anchor.TopRight, scaleBasis: ScaleBasis.BothWidth) { RelativeOffset = new Vector2(0.05f) }, style: "GUICancelButton") + { + UserData = i, + OnClicked = (btn, obj) => + { + JobList.Select((int)obj, true); + SwitchJob(btn, null); + if (JobSelectionFrame != null) { JobSelectionFrame.Visible = false; } + JobList.Deselect(); + + return false; + } + }; + + jobNamePreferences.Add(new Pair(jobPrefab.First.Identifier, jobPrefab.Second)); + } + else + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.6f), slot.RectTransform), (i + 1).ToString(), textColor: Color.White * (disableNext ? 0.15f : 0.5f), textAlignment: Alignment.Center, font: GUI.LargeFont) + { + CanBeFocused = false + }; + + if (!disableNext) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), slot.RectTransform, Anchor.BottomCenter), TextManager.Get("clicktoselectjob"), font: GUI.SmallFont, wrap: true, textAlignment: Alignment.Center) + { + CanBeFocused = false + }; + } + + disableNext = true; + } } + GameMain.Client.ForceNameAndJobUpdate(); if (!GameMain.Config.JobPreferences.SequenceEqual(jobNamePreferences)) { @@ -2067,9 +3031,17 @@ namespace Barotrauma .UserData as Submarine; //matching sub found and already selected, all good - if (sub != null && subList.SelectedData is Submarine selectedSub && selectedSub.MD5Hash?.Hash == md5Hash && System.IO.File.Exists(sub.FilePath)) + if (sub != null) { - return true; + if (subList == this.subList) + { + subPreviewContainer.ClearChildren(); + sub.CreatePreviewWindow(subPreviewContainer); + } + if (subList.SelectedData is Submarine selectedSub && selectedSub.MD5Hash?.Hash == md5Hash && System.IO.File.Exists(sub.FilePath)) + { + return true; + } } //sub not found, see if we have a sub with the same name diff --git a/Barotrauma/BarotraumaClient/Source/Screens/ParticleEditorScreen.cs b/Barotrauma/BarotraumaClient/Source/Screens/ParticleEditorScreen.cs index 4248b1f50..330683144 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/ParticleEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/ParticleEditorScreen.cs @@ -300,7 +300,7 @@ namespace Barotrauma //------------------------------------------------------- - spriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, GameMain.ScissorTestEnable); + spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); GUI.Draw(Cam, spriteBatch); diff --git a/Barotrauma/BarotraumaClient/Source/Screens/ServerListScreen.cs b/Barotrauma/BarotraumaClient/Source/Screens/ServerListScreen.cs index d6b23ad2e..a3ca7c6f5 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/ServerListScreen.cs @@ -6,12 +6,14 @@ using Microsoft.Xna.Framework.Graphics; using RestSharp; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Text; using System.Threading; +using System.Xml.Linq; namespace Barotrauma { @@ -27,7 +29,89 @@ namespace Barotrauma private readonly GUIButton joinButton; - private readonly GUITextBox clientNameBox, ipBox; + private readonly GUITextBox clientNameBox; + private ServerInfo selectedServer; + + //friends list + private readonly GUILayoutGroup friendsButtonHolder; + + private GUIButton friendsDropdownButton; + private GUIListBox friendsDropdown; + + private class FriendInfo + { + public UInt64 SteamID; + public string Name; + public Sprite Sprite; + public string Status; + public bool PlayingThisGame; + public string ConnectName; + public string ConnectEndpoint; + public UInt64 ConnectLobby; + + public bool InServer + { + get + { + return PlayingThisGame && !string.IsNullOrWhiteSpace(Status) && (!string.IsNullOrWhiteSpace(ConnectEndpoint) || ConnectLobby != 0); + } + } + } + private List friendsList; + private GUIFrame friendPopup; + private double friendsListUpdateTime; + + //favorite servers/history + private const string recentServersFile = "Data/recentservers.xml"; + private const string favoriteServersFile = "Data/favoriteservers.xml"; + private List favoriteServers; + private List recentServers; + + private readonly HashSet activePings = new HashSet(); + + private enum ServerListTab + { + All = 0, + Favorites = 1, + Recent = 2 + }; + private ServerListTab selectedTab; + private ServerListTab SelectedTab + { + get { return selectedTab; } + set + { + if (selectedTab == value) { return; } + var tabVals = Enum.GetValues(typeof(ServerListTab)); + for (int i = 0; i < tabVals.Length; i++) + { + tabButtons[i].Selected = false; + } + tabButtons[(int)value].Selected = true; + selectedTab = value; + FilterServers(); + } + } + private GUIButton[] tabButtons; + + //server playstyle and tags + public Sprite[] PlayStyleBanners + { + get; private set; + } + public Color[] PlayStyleColors + { + get; private set; + } + + public Dictionary PlayStyleIcons + { + get; private set; + } + public Dictionary PlayStyleIconColors + { + get; private set; + } private bool masterServerResponded; private IRestResponse masterServerResponse; @@ -40,10 +124,19 @@ namespace Barotrauma //filters private readonly GUITextBox searchBox; + private readonly GUITickBox filterSameVersion; private readonly GUITickBox filterPassword; private readonly GUITickBox filterIncompatible; private readonly GUITickBox filterFull; private readonly GUITickBox filterEmpty; + private readonly GUITickBox filterWhitelisted; + private readonly GUITickBox filterFriendlyFire; + private readonly GUITickBox filterKarma; + private readonly GUITickBox filterTraitor; + private readonly GUITickBox filterModded; + private readonly GUITickBox filterVoip; + private readonly List playStyleTickBoxes; + private readonly List gameModeTickBoxes; private string sortedBy; @@ -53,6 +146,7 @@ namespace Barotrauma private DateTime refreshDisableTimer; private bool waitingForRefresh; + private const float sidebarWidth = 0.2f; public ServerListScreen() { GameMain.Instance.OnResolutionChanged += OnResolutionChanged; @@ -78,9 +172,9 @@ namespace Barotrauma AutoScale = true }; - var infoHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.33f), topRow.RectTransform), isHorizontal: true) { RelativeSpacing = 0.05f, Stretch = true }; + var infoHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.33f), topRow.RectTransform), isHorizontal: true) { RelativeSpacing = 0.05f, Stretch = false }; - var clientNameHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), infoHolder.RectTransform)) { RelativeSpacing = 0.05f }; + var clientNameHolder = new GUILayoutGroup(new RectTransform(new Vector2(sidebarWidth, 1.0f), infoHolder.RectTransform)) { RelativeSpacing = 0.05f }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), clientNameHolder.RectTransform), TextManager.Get("YourName")); clientNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.5f), clientNameHolder.RectTransform), "") @@ -94,21 +188,38 @@ namespace Barotrauma { clientNameBox.Text = SteamManager.GetUsername(); } - - var ipBoxHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), infoHolder.RectTransform)) { RelativeSpacing = 0.05f }; - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), ipBoxHolder.RectTransform), TextManager.Get("ServerIP")); - ipBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.5f), ipBoxHolder.RectTransform), ""); - ipBox.OnTextChanged += (textBox, text) => { joinButton.Enabled = !string.IsNullOrEmpty(text); return true; }; - ipBox.OnSelected += (sender, key) => + clientNameBox.OnTextChanged += (textbox, text) => { - if (sender.UserData is ServerInfo) - { - sender.Text = ""; - sender.UserData = null; - } + GameMain.Config.PlayerName = text; + return true; }; + var tabButtonHolder = new GUIFrame(new RectTransform(new Vector2(1.0f - (sidebarWidth*2.0f), 1.25f), infoHolder.RectTransform), style: null); + + var tabVals = Enum.GetValues(typeof(ServerListTab)); + tabButtons = new GUIButton[tabVals.Length]; + int ind = 0; + foreach (ServerListTab tab in tabVals) + { + tabButtons[(int)tab] = new GUIButton(new RectTransform(new Vector2(0.25f, 0.5f), tabButtonHolder.RectTransform) + { + RelativeOffset = new Vector2(-0.06f + 0.22f * ind, 0.5f) + }, + TextManager.Get("ServerListTab."+tab.ToString()), style: "GUIButtonServerListTab"+(ind==0 ? "Left" : "Middle")) + { + OnClicked = (btn, usrdat) => + { + SelectedTab = tab; + return false; + } + }; + ind++; + } + + var friendsButtonFrame = new GUIFrame(new RectTransform(new Vector2(0.31f, 0.5f), tabButtonHolder.RectTransform) { RelativeOffset = new Vector2(0.60f, 0.5f) }, style: "GUIFrameServerListTabRight"); + friendsButtonHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.81f, 1.0f), friendsButtonFrame.RectTransform, Anchor.TopLeft) { RelativeOffset = new Vector2(0.19f, 0.0f) }, childAnchor: Anchor.TopLeft) { RelativeSpacing = 0.01f, IsHorizontal = true }; + friendsList = new List(); + //------------------------------------------------------------------------------------- // Bottom row //------------------------------------------------------------------------------------- @@ -121,45 +232,71 @@ namespace Barotrauma var serverListHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), bottomRow.RectTransform), isHorizontal: true) { - Stretch = true + OutlineColor = Color.Black }; + GUILayoutGroup serverListContainer = null; + GUIFrame filtersHolder = null; + GUIButton filterToggle = null; + + void RecalculateHolder() + { + float listContainerSubtract = filtersHolder.Visible ? sidebarWidth : 0.0f; + listContainerSubtract += serverPreview.Visible ? sidebarWidth : 0.0f; + + float toggleButtonsSubtract = 1.1f * filterToggle.Rect.Width / serverListHolder.Rect.Width; + listContainerSubtract += filterToggle.Visible ? toggleButtonsSubtract : 0.0f; + listContainerSubtract += serverPreviewToggleButton.Visible ? toggleButtonsSubtract : 0.0f; + + serverListContainer.RectTransform.RelativeSize = new Vector2(1.0f - listContainerSubtract, 1.0f); + serverListHolder.Recalculate(); + } + // filters ------------------------------------------- - var filters = new GUIFrame(new RectTransform(new Vector2(0.25f, 1.0f), serverListHolder.RectTransform, Anchor.Center), style: null) + filtersHolder = new GUIFrame(new RectTransform(new Vector2(sidebarWidth, 1.0f), serverListHolder.RectTransform, Anchor.Center), style: null) { Color = new Color(12, 14, 15, 255) * 0.5f, OutlineColor = Color.Black }; - var filterToggle = new GUIButton(new RectTransform(new Vector2(0.02f, 1.0f), serverListHolder.RectTransform, Anchor.CenterRight) { MinSize = new Point(20, 0) }, style: "UIToggleButton") + + var filters = new GUIListBox(new RectTransform(new Vector2(0.98f, 1.0f), filtersHolder.RectTransform, Anchor.CenterRight), style: null) + { + ScrollBarVisible = true + }; + + filterToggle = new GUIButton(new RectTransform(new Vector2(0.01f, 1.0f), serverListHolder.RectTransform, Anchor.CenterRight) { MinSize = new Point(20, 0) }, style: "UIToggleButton") { OnClicked = (btn, userdata) => { - filters.RectTransform.RelativeSize = new Vector2(0.25f, 1.0f); - filters.Visible = !filters.Visible; - filters.IgnoreLayoutGroups = !filters.Visible; - serverListHolder.Recalculate(); - btn.Children.ForEach(c => c.SpriteEffects = !filters.Visible ? SpriteEffects.None : SpriteEffects.FlipHorizontally); + filtersHolder.RectTransform.RelativeSize = new Vector2(sidebarWidth, 1.0f); + filtersHolder.Visible = !filtersHolder.Visible; + filtersHolder.IgnoreLayoutGroups = !filtersHolder.Visible; + + RecalculateHolder(); + + btn.Children.ForEach(c => c.SpriteEffects = !filtersHolder.Visible ? SpriteEffects.None : SpriteEffects.FlipHorizontally); return true; } }; filterToggle.Children.ForEach(c => c.SpriteEffects = SpriteEffects.FlipHorizontally); - var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.99f), filters.RectTransform, Anchor.Center)) + /*var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.99f), filters.Content.RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.015f - }; + };*/ - var filterTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), filterContainer.RectTransform), TextManager.Get("FilterServers"), font: GUI.LargeFont) + var filterTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), filters.Content.RectTransform), TextManager.Get("FilterServers"), font: GUI.LargeFont) { Padding = Vector4.Zero, - AutoScale = true + AutoScale = true, + CanBeFocused = false }; float elementHeight = 0.05f; - var searchHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, elementHeight), filterContainer.RectTransform), isHorizontal: true) { Stretch = true }; + var searchHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), isHorizontal: true) { Stretch = true }; var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), searchHolder.RectTransform), TextManager.Get("Search") + "..."); searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), searchHolder.RectTransform), ""); @@ -167,49 +304,149 @@ namespace Barotrauma searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; searchBox.OnTextChanged += (txtBox, txt) => { FilterServers(); return true; }; - var filterHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform)) { RelativeSpacing = 0.005f }; + //var filterHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), filters.Content.RectTransform)) { RelativeSpacing = 0.005f }; List filterTextList = new List(); - filterPassword = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filterHolder.RectTransform), TextManager.Get("FilterPassword")) + + filterSameVersion = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterSameVersion")) + { + ToolTip = TextManager.Get("FilterSameVersion"), + Selected = true, + OnSelected = (tickBox) => { FilterServers(); return true; } + }; + filterTextList.Add(filterSameVersion.TextBlock); + + filterPassword = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterPassword")) { ToolTip = TextManager.Get("FilterPassword"), OnSelected = (tickBox) => { FilterServers(); return true; } }; filterTextList.Add(filterPassword.TextBlock); - filterIncompatible = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filterHolder.RectTransform), TextManager.Get("FilterIncompatibleServers")) + + filterIncompatible = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterIncompatibleServers")) { ToolTip = TextManager.Get("FilterIncompatibleServers"), OnSelected = (tickBox) => { FilterServers(); return true; } }; filterTextList.Add(filterIncompatible.TextBlock); - filterFull = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filterHolder.RectTransform), TextManager.Get("FilterFullServers")) + + filterFull = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterFullServers")) { ToolTip = TextManager.Get("FilterFullServers"), OnSelected = (tickBox) => { FilterServers(); return true; } }; filterTextList.Add(filterFull.TextBlock); - filterEmpty = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filterHolder.RectTransform), TextManager.Get("FilterEmptyServers")) + + filterEmpty = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterEmptyServers")) { ToolTip = TextManager.Get("FilterEmptyServers"), OnSelected = (tickBox) => { FilterServers(); return true; } }; filterTextList.Add(filterEmpty.TextBlock); - filterContainer.RectTransform.SizeChanged += () => + filterWhitelisted = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterWhitelistedServers")) { - filterContainer.RectTransform.RecalculateChildren(true, true); + ToolTip = TextManager.Get("FilterWhitelistedServers"), + OnSelected = (tickBox) => { FilterServers(); return true; } + }; + filterTextList.Add(filterWhitelisted.TextBlock); + + // Filter Tags + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("servertags")) + { + CanBeFocused = false + }; + + filterKarma = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("servertag.karma.true")) + { + ToolTip = TextManager.Get("servertag.karma.true"), + OnSelected = (tickBox) => { FilterServers(); return true; } + }; + filterTextList.Add(filterKarma.TextBlock); + + filterTraitor = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("servertag.traitors.true")) + { + ToolTip = TextManager.Get("servertag.traitors.true"), + OnSelected = (tickBox) => { FilterServers(); return true; } + }; + filterTextList.Add(filterTraitor.TextBlock); + + filterFriendlyFire = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("servertag.friendlyfire.false")) + { + ToolTip = TextManager.Get("servertag.friendlyfire.false"), + OnSelected = (tickBox) => { FilterServers(); return true; } + }; + filterTextList.Add(filterFriendlyFire.TextBlock); + + filterVoip = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("servertag.voip.true")) + { + ToolTip = TextManager.Get("servertag.voip.true"), + OnSelected = (tickBox) => { FilterServers(); return true; } + }; + filterTextList.Add(filterVoip.TextBlock); + + + filterModded = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("servertag.modded.true")) + { + ToolTip = TextManager.Get("servertag.modded.true"), + OnSelected = (tickBox) => { FilterServers(); return true; } + }; + filterTextList.Add(filterModded.TextBlock); + + // Play Style Selection + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("ServerSettingsPlayStyle")) + { + CanBeFocused = false + }; + + playStyleTickBoxes = new List(); + foreach (PlayStyle playStyle in Enum.GetValues(typeof(PlayStyle))) + { + var selectionTick = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("servertag." + playStyle)) + { + ToolTip = TextManager.Get("servertag." + playStyle), + Selected = true, + OnSelected = (tickBox) => { FilterServers(); return true; }, + UserData = playStyle + }; + playStyleTickBoxes.Add(selectionTick); + filterTextList.Add(selectionTick.TextBlock); + } + + // Game mode Selection + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("gamemode")) { CanBeFocused = false }; + + gameModeTickBoxes = new List(); + foreach (GameModePreset mode in GameModePreset.List) + { + if (mode.IsSinglePlayer) continue; + + var selectionTick = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), mode.Name) + { + ToolTip = mode.Name, + Selected = true, + OnSelected = (tickBox) => { FilterServers(); return true; }, + UserData = mode.Name + }; + gameModeTickBoxes.Add(selectionTick); + filterTextList.Add(selectionTick.TextBlock); + } + + filters.Content.RectTransform.SizeChanged += () => + { + filters.Content.RectTransform.RecalculateChildren(true, true); filterTextList.ForEach(t => t.Text = t.ToolTip); GUITextBlock.AutoScaleAndNormalize(filterTextList); if (filterTextList[0].TextScale < 0.8f) { filterTextList.ForEach(t => t.TextScale = 1.0f); - filterTextList.ForEach(t => t.Text = ToolBox.LimitString(t.Text, t.Font, (int)(filterContainer.Rect.Width * 0.8f))); + filterTextList.ForEach(t => t.Text = ToolBox.LimitString(t.Text, t.Font, (int)(filters.Content.Rect.Width * 0.8f))); } }; // server list --------------------------------------------------------------------- - var serverListContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), serverListHolder.RectTransform)) { Stretch = true }; + serverListContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), serverListHolder.RectTransform)) { Stretch = true }; labelHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.99f, 0.05f), serverListContainer.RectTransform) { MinSize = new Point(0, 15) }, isHorizontal: true) @@ -257,16 +494,15 @@ namespace Barotrauma if (obj is ServerInfo serverInfo) { joinButton.Enabled = true; - ipBox.UserData = serverInfo; - ipBox.Text = serverInfo.ServerName; + selectedServer = serverInfo; if (!serverPreview.Visible) { - serverPreview.RectTransform.RelativeSize = new Vector2(0.3f, 1.0f); + serverPreview.RectTransform.RelativeSize = new Vector2(sidebarWidth, 1.0f); serverPreviewToggleButton.Visible = true; serverPreviewToggleButton.IgnoreLayoutGroups = false; serverPreview.Visible = true; serverPreview.IgnoreLayoutGroups = false; - serverListHolder.Recalculate(); + RecalculateHolder(); } serverInfo.CreatePreviewWindow(serverPreview); btn.Children.ForEach(c => c.SpriteEffects = serverPreview.Visible ? SpriteEffects.None : SpriteEffects.FlipHorizontally); @@ -277,21 +513,23 @@ namespace Barotrauma //server preview panel -------------------------------------------------- - serverPreviewToggleButton = new GUIButton(new RectTransform(new Vector2(0.02f, 1.0f), serverListHolder.RectTransform, Anchor.CenterRight) { MinSize = new Point(20, 0) }, style: "UIToggleButton") + serverPreviewToggleButton = new GUIButton(new RectTransform(new Vector2(0.01f, 1.0f), serverListHolder.RectTransform, Anchor.CenterRight) { MinSize = new Point(20, 0) }, style: "UIToggleButton") { Visible = false, OnClicked = (btn, userdata) => { - serverPreview.RectTransform.RelativeSize = new Vector2(0.25f, 1.0f); + serverPreview.RectTransform.RelativeSize = new Vector2(0.2f, 1.0f); serverPreview.Visible = !serverPreview.Visible; serverPreview.IgnoreLayoutGroups = !serverPreview.Visible; - serverListHolder.Recalculate(); + + RecalculateHolder(); + btn.Children.ForEach(c => c.SpriteEffects = serverPreview.Visible ? SpriteEffects.None : SpriteEffects.FlipHorizontally); return true; } }; - serverPreview = new GUIFrame(new RectTransform(new Vector2(0.3f, 1.0f), serverListHolder.RectTransform, Anchor.Center), style: null) + serverPreview = new GUIFrame(new RectTransform(new Vector2(sidebarWidth, 1.0f), serverListHolder.RectTransform, Anchor.Center), style: null) { Color = new Color(12, 14, 15, 255) * 0.5f, OutlineColor = Color.Black, @@ -320,31 +558,32 @@ namespace Barotrauma OnClicked = (btn, userdata) => { RefreshServers(); return true; } }; - /*var directJoinButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform), + var directJoinButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform), TextManager.Get("serverlistdirectjoin"), style: "GUIButtonLarge") { OnClicked = (btn, userdata) => { ShowDirectJoinPrompt(); return true; } - };*/ + }; joinButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform), TextManager.Get("ServerListJoin"), style: "GUIButtonLarge") { OnClicked = (btn, userdata) => { - if (ipBox.UserData is ServerInfo selectedServer) + if (selectedServer != null) { - if (selectedServer.LobbyID == 0) + if (!string.IsNullOrWhiteSpace(selectedServer.IP) && !string.IsNullOrWhiteSpace(selectedServer.Port) && int.TryParse(selectedServer.Port, out _)) { JoinServer(selectedServer.IP + ":" + selectedServer.Port, selectedServer.ServerName); } - else + else if (selectedServer.LobbyID != 0) { Steam.SteamManager.JoinLobby(selectedServer.LobbyID, true); } - } - else if (!string.IsNullOrEmpty(ipBox.Text)) - { - JoinServer(ipBox.Text, ""); + else + { + //TODO: error message here? + return false; + } } return true; }, @@ -367,10 +606,188 @@ namespace Barotrauma { labelText.Text = ToolBox.LimitString(labelText.ToolTip, labelText.Font, labelText.Rect.Width); } + RecalculateHolder(); }; button.SelectedColor = button.Color; refreshDisableTimer = DateTime.Now; + + //playstyle banners + PlayStyleBanners = new Sprite[Enum.GetValues(typeof(PlayStyle)).Length]; + PlayStyleColors = new Color[Enum.GetValues(typeof(PlayStyle)).Length]; + PlayStyleIcons = new Dictionary(); + PlayStyleIconColors = new Dictionary(); + + XDocument playStylesDoc = XMLExtensions.TryLoadXml("Content/UI/Server/PlayStyles.xml"); + + XElement rootElement = playStylesDoc.Root; + foreach (var element in rootElement.Elements()) + { + switch (element.Name.ToString().ToLowerInvariant()) + { + case "playstylebanner": + if (Enum.TryParse(element.GetAttributeString("identifier", ""), out PlayStyle playStyle)) + { + PlayStyleBanners[(int)playStyle] = new Sprite(element, lazyLoad: true); + PlayStyleColors[(int)playStyle] = element.GetAttributeColor("color", Color.White); + } + break; + case "playstyleicon": + string identifier = element.GetAttributeString("identifier", ""); + if (string.IsNullOrEmpty(identifier)) { continue; } + PlayStyleIcons[identifier] = new Sprite(element, lazyLoad: true); + PlayStyleIconColors[identifier] = element.GetAttributeColor("color", Color.White); + break; + } + } + + //recent and favorite servers + ReadServerMemFromFile(recentServersFile, ref recentServers); + ReadServerMemFromFile(favoriteServersFile, ref favoriteServers); + recentServers.ForEach(s => s.Recent = true); + favoriteServers.ForEach(s => s.Favorite = true); + + SelectedTab = ServerListTab.All; + tabButtons[(int)selectedTab].Selected = true; + + RecalculateHolder(); + } + + private void ReadServerMemFromFile(string file, ref List servers) + { + if (servers == null) { servers = new List(); } + + if (!File.Exists(file)) { return; } + + XDocument doc = XMLExtensions.TryLoadXml(file); + if (doc == null) { return; } + + foreach (XElement element in doc.Root.Elements()) + { + if (element.Name != "ServerInfo") { continue; } + servers.Add(ServerInfo.FromXElement(element)); + } + } + + private void WriteServerMemToFile(string file, List servers) + { + if (servers == null) { return; } + + XDocument doc = new XDocument(); + XElement rootElement = new XElement("servers"); + doc.Add(rootElement); + + foreach (ServerInfo info in servers) + { + rootElement.Add(info.ToXElement()); + } + + doc.Save(file); + } + + public ServerInfo UpdateServerInfoWithServerSettings(object endpoint, ServerSettings serverSettings) + { + UInt64 steamId = 0; + string ip = ""; string port = ""; + if (endpoint is UInt64 id) { steamId = id; } + else if (endpoint is string strEndpoint) + { + string[] address = strEndpoint.Split(':'); + if (address.Length == 1) + { + ip = strEndpoint; + port = NetConfig.DefaultPort.ToString(); + } + else + { + ip = string.Join(":", address.Take(address.Length - 1)); + port = address[address.Length - 1]; + } + } + + bool isInfoNew = false; + ServerInfo info = serverList.Content.FindChild(d => (d.UserData is ServerInfo serverInfo) && serverInfo != null && + (steamId != 0 ? steamId == serverInfo.OwnerID : (ip == serverInfo.IP && port == serverInfo.Port)))?.UserData as ServerInfo; + if (info == null) + { + isInfoNew = true; + info = new ServerInfo(); + } + + info.ServerName = serverSettings.ServerName; + info.ServerMessage = serverSettings.ServerMessageText; + info.OwnerID = steamId; + info.LobbyID = SteamManager.LobbyID; + info.IP = ip; + info.Port = port; + info.GameMode = GameMain.NetLobbyScreen.SelectedMode?.Identifier ?? ""; + info.GameStarted = Screen.Selected != GameMain.NetLobbyScreen; + info.GameVersion = GameMain.Version.ToString(); + info.MaxPlayers = serverSettings.MaxPlayers; + info.PlayStyle = PlayStyle.SomethingDifferent; + info.RespondedToSteamQuery = true; + info.UsingWhiteList = serverSettings.Whitelist.Enabled; + info.TraitorsEnabled = serverSettings.TraitorsEnabled; + info.SubSelectionMode = serverSettings.SubSelectionMode; + info.ModeSelectionMode = serverSettings.ModeSelectionMode; + info.VoipEnabled = serverSettings.VoiceChatEnabled; + info.FriendlyFireEnabled = serverSettings.AllowFriendlyFire; + info.KarmaEnabled = serverSettings.KarmaEnabled; + info.PlayerCount = GameMain.Client.ConnectedClients.Count; + info.PingChecked = false; + info.HasPassword = serverSettings.HasPassword; + + if (isInfoNew) + { + AddToServerList(info); + } + + return info; + } + + public void AddToRecentServers(ServerInfo info) + { + info.Recent = true; + ServerInfo existingInfo = recentServers.Find(serverInfo => info.OwnerID == serverInfo.OwnerID && (info.OwnerID != 0 ? true : (info.IP == serverInfo.IP && info.Port == serverInfo.Port))); + if (existingInfo == null) + { + recentServers.Add(info); + } + else + { + int index = recentServers.IndexOf(existingInfo); + recentServers[index] = info; + } + + WriteServerMemToFile(recentServersFile, recentServers); + } + + public void AddToFavoriteServers(ServerInfo info) + { + info.Favorite = true; + ServerInfo existingInfo = favoriteServers.Find(serverInfo => info.OwnerID == serverInfo.OwnerID && (info.OwnerID != 0 ? true : (info.IP == serverInfo.IP && info.Port == serverInfo.Port))); + if (existingInfo == null) + { + favoriteServers.Add(info); + } + else + { + int index = favoriteServers.IndexOf(existingInfo); + favoriteServers[index] = info; + } + + WriteServerMemToFile(favoriteServersFile, favoriteServers); + } + + public void RemoveFromFavoriteServers(ServerInfo info) + { + info.Favorite = false; + ServerInfo existingInfo = favoriteServers.Find(serverInfo => info.OwnerID == serverInfo.OwnerID && (info.OwnerID != 0 ? true : (info.IP == serverInfo.IP && info.Port == serverInfo.Port))); + if (existingInfo != null) + { + favoriteServers.Remove(existingInfo); + WriteServerMemToFile(favoriteServersFile, favoriteServers); + } } private void OnResolutionChanged() @@ -479,6 +896,7 @@ namespace Barotrauma public override void Select() { base.Select(); + SelectedTab = ServerListTab.All; RefreshServers(); } @@ -491,6 +909,24 @@ namespace Barotrauma } } + public override void Update(double deltaTime) + { + base.Update(deltaTime); + + UpdateFriendsList(); + + if (PlayerInput.LeftButtonClicked()) + { + friendPopup = null; + if (friendsDropdown != null && friendsDropdownButton != null && + !friendsDropdown.Rect.Contains(PlayerInput.MousePosition) && + !friendsDropdownButton.Rect.Contains(PlayerInput.MousePosition)) + { + friendsDropdown.Visible = false; + } + } + } + private void FilterServers() { serverList.Content.RemoveChild(serverList.Content.FindChild("noresults")); @@ -512,10 +948,42 @@ namespace Barotrauma child.Visible = serverInfo.ServerName.ToLowerInvariant().Contains(searchBox.Text.ToLowerInvariant()) && + (!filterSameVersion.Selected || (remoteVersion != null && NetworkMember.IsCompatible(remoteVersion, GameMain.Version))) && (!filterPassword.Selected || !serverInfo.HasPassword) && (!filterIncompatible.Selected || !incompatible) && (!filterFull.Selected || serverInfo.PlayerCount < serverInfo.MaxPlayers) && - (!filterEmpty.Selected || serverInfo.PlayerCount > 0); + (!filterEmpty.Selected || serverInfo.PlayerCount > 0) && + (!filterWhitelisted.Selected || serverInfo.UsingWhiteList == true) && + (!filterKarma.Selected || serverInfo.KarmaEnabled == true) && + (!filterFriendlyFire.Selected || serverInfo.FriendlyFireEnabled == false) && + (!filterTraitor.Selected || serverInfo.TraitorsEnabled == YesNoMaybe.Yes || serverInfo.TraitorsEnabled == YesNoMaybe.Maybe) && + (!filterVoip.Selected || serverInfo.VoipEnabled == true) && + (!filterModded.Selected || serverInfo.GetPlayStyleTags().Any(t => t.Contains("modded.true"))) && + ((selectedTab == ServerListTab.All && (serverInfo.LobbyID != 0 || !string.IsNullOrWhiteSpace(serverInfo.Port))) || + (selectedTab == ServerListTab.Recent && serverInfo.Recent) || + (selectedTab == ServerListTab.Favorites && serverInfo.Favorite)) && + (remoteVersion != null && remoteVersion <= GameMain.Version); + + foreach (GUITickBox tickBox in playStyleTickBoxes) + { + var playStyle = (PlayStyle)tickBox.UserData; + + if (!tickBox.Selected && serverInfo.PlayStyle == playStyle) + { + child.Visible = false; + break; + } + } + + foreach (GUITickBox tickBox in gameModeTickBoxes) + { + var gameMode = (string)tickBox.UserData; + if (!tickBox.Selected && (serverInfo.GameMode == gameMode.ToLowerInvariant() || serverInfo.GameMode == gameMode)) + { + child.Visible = false; + break; + } + } } if (serverList.Content.Children.All(c => !c.Visible)) @@ -530,26 +998,26 @@ namespace Barotrauma serverList.UpdateScrollBarSize(); } - /*private void ShowDirectJoinPrompt() + private void ShowDirectJoinPrompt() { - var msgBox = new GUIMessageBox(TextManager.Get("ServerListDirectJoin"), "", new string[] { TextManager.Get("OK"), TextManager.Get("Cancel") }, + var msgBox = new GUIMessageBox(TextManager.Get("ServerListDirectJoin"), "", new string[] { TextManager.Get("ServerListJoin"), TextManager.Get("Cancel") }, relativeSize: new Vector2(0.25f, 0.2f), minSize: new Point(400, 150)); - var content = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.3f), msgBox.InnerFrame.RectTransform, Anchor.Center) { MinSize = new Point(0, 50) }) + var content = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.5f), msgBox.InnerFrame.RectTransform, Anchor.Center) { MinSize = new Point(0, 50) }) { IgnoreLayoutGroups = true, Stretch = true, RelativeSpacing = 0.05f }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform), TextManager.Get("ServerIP")); - var ipBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform)); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.33f), content.RectTransform), TextManager.Get("ServerEndpoint")); + var endpointBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.33f), content.RectTransform)); var okButton = msgBox.Buttons[0]; okButton.Enabled = false; okButton.OnClicked = (btn, userdata) => { - JoinServer(ipBox.Text, ""); + JoinServer(endpointBox.Text, ""); msgBox.Close(); return true; }; @@ -557,28 +1025,376 @@ namespace Barotrauma var cancelButton = msgBox.Buttons[1]; cancelButton.OnClicked = msgBox.Close; - ipBox.OnTextChanged += (textBox, text) => + endpointBox.OnTextChanged += (textBox, text) => { okButton.Enabled = !string.IsNullOrEmpty(text); return true; }; - }*/ + + var spacingLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.33f), content.RectTransform), true); + + new GUIFrame(new RectTransform(new Vector2(0.5f, 1.0f), spacingLayoutGroup.RectTransform), null); + + var addToFavoritesButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), spacingLayoutGroup.RectTransform), TextManager.Get("AddToFavorites")); + addToFavoritesButton.OnClicked = (button, userdata) => + { + UInt64 steamId = SteamManager.SteamIDStringToUInt64(endpointBox.Text); + string ip = ""; int port = 0; + if (steamId == 0) + { + string hostIP = endpointBox.Text; + + string[] address = hostIP.Split(':'); + if (address.Length == 1) + { + ip = hostIP; + port = NetConfig.DefaultPort; + } + else + { + ip = string.Join(":", address.Take(address.Length - 1)); + if (!int.TryParse(address[address.Length - 1], out port)) + { + DebugConsole.ThrowError("Invalid port: " + address[address.Length - 1] + "!"); + port = NetConfig.DefaultPort; + } + } + } + + //TODO: add a better way to get the query port, right now we're just assuming that it'll always be the default + ServerInfo serverInfo = new ServerInfo() + { + ServerName = "Server", + OwnerID = steamId, + IP = ip, + Port = port.ToString(), + QueryPort = NetConfig.DefaultQueryPort.ToString(), + GameVersion = GameMain.Version.ToString(), + PlayStyle = PlayStyle.Serious + }; + + var serverFrame = serverList.Content.FindChild(d => (d.UserData is ServerInfo info) && + info.OwnerID == serverInfo.OwnerID && + (serverInfo.OwnerID != 0 ? true : (info.IP == serverInfo.IP && info.Port == serverInfo.Port))); + + if (serverFrame != null) + { + serverInfo = serverFrame.UserData as ServerInfo; + } + else + { + AddToServerList(serverInfo); + } + + AddToFavoriteServers(serverInfo); + + SelectedTab = ServerListTab.Favorites; + FilterServers(); + + serverInfo.QueryLiveInfo(UpdateServerInfo); + + msgBox.Close(); + return false; + }; + } + + private bool JoinFriend(GUIButton button, object userdata) + { + FriendInfo info = userdata as FriendInfo; + + if (info.InServer) + { + if (info.ConnectLobby != 0) + { + GameMain.Instance.ConnectLobby = info.ConnectLobby; + GameMain.Instance.ConnectEndpoint = null; + GameMain.Instance.ConnectName = null; + } + else + { + GameMain.Instance.ConnectLobby = 0; + GameMain.Instance.ConnectEndpoint = info.ConnectEndpoint; + GameMain.Instance.ConnectName = info.ConnectName; + } + } + return false; + } + + private bool OpenFriendPopup(GUIButton button, object userdata) + { + FriendInfo info = userdata as FriendInfo; + + if (info.InServer) + { + friendPopup = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas)); + var serverNameText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), friendPopup.RectTransform), info.ConnectName ?? "[Unnamed]"); + var joinButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), friendPopup.RectTransform, Anchor.TopRight), TextManager.Get("ServerListJoin")) + { + UserData = info + }; + joinButton.OnClicked = JoinFriend; + + Vector2 frameDims = joinButton.Font.MeasureString(info.ConnectName ?? "[Unnamed]"); + frameDims.X /= 0.6f; + frameDims.Y *= 1.5f; + friendPopup.RectTransform.NonScaledSize = frameDims.ToPoint(); + friendPopup.RectTransform.RelativeOffset = Vector2.Zero; + friendPopup.RectTransform.AbsoluteOffset = PlayerInput.MousePosition.ToPoint(); + friendPopup.RectTransform.RecalculateChildren(true); + friendPopup.RectTransform.SetPosition(Anchor.TopLeft); + } + + return false; + } + + private void UpdateFriendsList() + { + if (!SteamManager.IsInitialized) { return; } + + if (friendsListUpdateTime > Timing.TotalTime) { return; } + friendsListUpdateTime = Timing.TotalTime + 5.0; + + float prevDropdownScroll = friendsDropdown?.ScrollBar.BarScrollValue ?? 0.0f; + + if (friendsDropdown == null) { + friendsDropdown = new GUIListBox(new RectTransform(Vector2.One, GUI.Canvas)) + { + OutlineColor = Color.Black, + Visible = false + }; + } + friendsDropdown.ClearChildren(); + + Facepunch.Steamworks.Friends.AvatarSize avatarSize = Facepunch.Steamworks.Friends.AvatarSize.Large; + if (friendsButtonHolder.RectTransform.Rect.Height <= 24) + { + avatarSize = Facepunch.Steamworks.Friends.AvatarSize.Small; + } + else if (friendsButtonHolder.RectTransform.Rect.Height <= 48) + { + avatarSize = Facepunch.Steamworks.Friends.AvatarSize.Medium; + } + + SteamManager.Instance.Friends.Refresh(); + + for (int i = friendsList.Count - 1; i >= 0; i--) + { + var friend = friendsList[i]; + if (!SteamManager.Instance.Friends.AllFriends.Any(g => g.Id == friend.SteamID && g.IsOnline)) + { + friend.Sprite?.Remove(); + friendsList.RemoveAt(i); + } + } + + foreach (var friend in SteamManager.Instance.Friends.AllFriends) + { + if (!friend.IsOnline) { continue; } + + FriendInfo info = friendsList.Find(f => f.SteamID == friend.Id); + if (info == null) + { + info = new FriendInfo() + { + SteamID = friend.Id + }; + friendsList.Insert(0, info); + } + + if (info.Sprite == null) + { + var avatarImage = friend.GetAvatar(avatarSize); + if (avatarImage != null) + { + const int desaturatedWeight = 180; + + byte[] avatarData = (byte[])avatarImage.Data.Clone(); + for (int i=0;i 255 ? (byte)255 : (byte)chn0; + avatarData[i + 1] = chn1 > 255 ? (byte)255 : (byte)chn1; + avatarData[i + 2] = chn2 > 255 ? (byte)255 : (byte)chn2; + avatarData[i + 3] = chn3 > 255 ? (byte)255 : (byte)chn3; + } + //TODO: create an avatar atlas? + var avatarTexture = new Texture2D(GameMain.Instance.GraphicsDevice, avatarImage.Width, avatarImage.Height); + avatarTexture.SetData(avatarData); + + info.Sprite = new Sprite(avatarTexture, null, null); + } + } + + info.Name = friend.Name; + + info.ConnectName = null; + info.ConnectEndpoint = null; + info.ConnectLobby = 0; + + info.PlayingThisGame = friend.IsPlayingThisGame; + + if (friend.IsPlayingThisGame) + { + info.Status = friend.GetRichPresence("status") ?? ""; + string connectCommand = friend.GetRichPresence("connect") ?? ""; + + ToolBox.ParseConnectCommand(connectCommand.Split(' '), out info.ConnectName, out info.ConnectEndpoint, out info.ConnectLobby); + } + else + { + info.Status = TextManager.Get(friend.IsPlaying ? "FriendPlayingAnotherGame" : "FriendNotPlaying"); + } + } + + friendsList.Sort((a, b) => + { + if (a.InServer && !b.InServer) { return -1; } + if (b.InServer && !a.InServer) { return 1; } + if (a.PlayingThisGame && !b.PlayingThisGame) { return -1; } + if (b.PlayingThisGame && !a.PlayingThisGame) { return 1; } + return 0; + }); + + friendsButtonHolder.ClearChildren(); + + if (friendsList.Count > 0) + { + friendsDropdownButton = new GUIButton(new RectTransform(Vector2.One, friendsButtonHolder.RectTransform, Anchor.BottomRight, Pivot.BottomRight, scaleBasis: ScaleBasis.BothHeight), "\u2022 \u2022 \u2022", style: "GUIButtonFriendsDropdown") + { + Font = GUI.ObjectiveNameFont, + OnClicked = (button, udt) => + { + friendsDropdown.RectTransform.NonScaledSize = new Point(friendsButtonHolder.Rect.Height * 5 * 166 / 100, friendsButtonHolder.Rect.Height * 4 * 166 / 100); + friendsDropdown.RectTransform.RelativeOffset = new Vector2(0.295f, 0.235f); + friendsDropdown.RectTransform.RecalculateChildren(true); + friendsDropdown.RectTransform.SetPosition(Anchor.TopRight); + + friendsDropdown.Visible = !friendsDropdown.Visible; + return false; + } + }; + } + else + { + friendsDropdownButton = null; + friendsDropdown.Visible = false; + } + + int buttonCount = 0; + + for (int i = 0; i < friendsList.Count; i++) + { + var friend = friendsList[i]; + buttonCount++; + + if (buttonCount <= 5) + { + string style = "GUIButtonFriendNotPlaying"; + if (friend.InServer) + { + style = "GUIButtonFriendPlaying"; + } + else + { + style = friend.PlayingThisGame ? "GUIButtonFriendPlaying" : "GUIButtonFriendNotPlaying"; + } + + var guiButton = new GUIButton(new RectTransform(Vector2.One, friendsButtonHolder.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: style) + { + UserData = friend, + OnClicked = OpenFriendPopup + }; + guiButton.ToolTip = friend.Name + "\n" + friend.Status; + + if (friend.Sprite != null) + { + Color BrightenColor(Color color) + { + Vector3 hls = ToolBox.RgbToHLS(color); + hls.Y = hls.Y * 0.3f + 0.7f; + hls.Z = hls.Z * 0.6f + 0.4f; + + return ToolBox.HLSToRGB(hls); + } + + var imgColor = BrightenColor(guiButton.Color); + var imgHoverColor = BrightenColor(guiButton.HoverColor); + var imgSelectColor = BrightenColor(guiButton.SelectedColor); + var imgPressColor = BrightenColor(guiButton.PressedColor); + var guiImage = new GUIImage(new RectTransform(Vector2.One * 0.925f, guiButton.RectTransform, Anchor.Center) { RelativeOffset = new Vector2(0.025f, 0.025f) }, friend.Sprite, null, true) + { + Color = imgColor, + HoverColor = imgHoverColor, + SelectedColor = imgSelectColor, + PressedColor = imgPressColor, + CanBeFocused = false + }; + guiImage = new GUIImage(new RectTransform(Vector2.One * 0.925f, guiButton.RectTransform, Anchor.Center) { RelativeOffset = new Vector2(0.025f, 0.025f) }, friend.Sprite, null, true) + { + Color = Color.White * 0.8f, + HoverColor = Color.White * 0.8f, + SelectedColor = Color.White * 0.8f, + PressedColor = Color.White * 0.8f, + BlendState = BlendState.Additive, + CanBeFocused = false + }; + } + } + + var friendFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.167f), friendsDropdown.Content.RectTransform), style: "GUIFrameFriendsDropdown"); + var guiImage2TheSequel = new GUIImage(new RectTransform(Vector2.One * 0.9f, friendFrame.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.BothHeight) { RelativeOffset = new Vector2(0.02f, 0.02f) } , friend.Sprite, null, true); + + var textBlock = new GUITextBlock(new RectTransform(Vector2.One * 0.8f, friendFrame.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.BothHeight) { RelativeOffset = new Vector2(1.0f / 7.7f, 0.0f) }, friend.Name + "\n" + friend.Status) + { + Font = GUI.SmallFont + }; + + if (friend.InServer) + { + var joinButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.6f), friendFrame.RectTransform, Anchor.CenterRight) { RelativeOffset = new Vector2(0.05f, 0.0f) }, TextManager.Get("ServerListJoin"), style: "GUIButtonJoinFriend") + { + UserData = friend + }; + joinButton.OnClicked = JoinFriend; + } + } + + friendsDropdown.RectTransform.NonScaledSize = new Point(friendsButtonHolder.Rect.Height * 5 * 166 / 100, friendsButtonHolder.Rect.Height * 4 * 166 / 100); + friendsDropdown.RectTransform.RelativeOffset = new Vector2(0.295f, 0.235f); + friendsDropdown.RectTransform.RecalculateChildren(true); + friendsDropdown.RectTransform.SetPosition(Anchor.TopRight); + + friendsDropdown.ScrollBar.BarScrollValue = prevDropdownScroll; + } private void RefreshServers() { if (waitingForRefresh) { return; } + + friendsListUpdateTime = Timing.TotalTime - 1.0; + UpdateFriendsList(); + serverList.ClearChildren(); serverPreview.ClearChildren(); joinButton.Enabled = false; - ipBox.UserData = null; - ipBox.Text = ""; + selectedServer = null; new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), serverList.Content.RectTransform), TextManager.Get("RefreshingServerList"), textAlignment: Alignment.Center) { CanBeFocused = false }; - + CoroutineManager.StartCoroutine(WaitForRefresh()); } @@ -602,6 +1418,15 @@ namespace Barotrauma CanBeFocused = false }; } + else + { + List knownServers = recentServers.Concat(favoriteServers).ToList(); + foreach (ServerInfo info in knownServers) + { + AddToServerList(info); + info.QueryLiveInfo(UpdateServerInfo); + } + } } else { @@ -689,7 +1514,10 @@ namespace Barotrauma private void AddToServerList(ServerInfo serverInfo) { var serverFrame = serverList.Content.FindChild(d => (d.UserData is ServerInfo info) && - (info.LobbyID==serverInfo.LobbyID && info.IP==serverInfo.IP && info.Port==serverInfo.Port)); + (info.LobbyID == serverInfo.LobbyID || + (info.LobbyID == 0 && info.OwnerID == serverInfo.OwnerID && + serverInfo.OwnerVerified)) && + (serverInfo.OwnerID != 0 ? true : (info.IP == serverInfo.IP && info.Port == serverInfo.Port))); if (serverFrame == null) { @@ -704,8 +1532,33 @@ namespace Barotrauma //RelativeSpacing = 0.02f }; } + else + { + int index = recentServers.IndexOf(serverFrame.UserData as ServerInfo); + if (index >= 0) + { + recentServers[index] = serverInfo; + serverInfo.Recent = true; + } + index = favoriteServers.IndexOf(serverFrame.UserData as ServerInfo); + if (index >= 0) + { + favoriteServers[index] = serverInfo; + serverInfo.Favorite = true; + } + } serverFrame.UserData = serverInfo; - + + if (serverInfo.OwnerVerified) + { + DebugConsole.NewMessage(serverInfo.OwnerID + " verified!"); + var childrenToRemove = serverList.Content.FindChildren(c => (c.UserData is ServerInfo info) && info != serverInfo && info.OwnerID == serverInfo.OwnerID).ToList(); + foreach (var child in childrenToRemove) + { + serverList.Content.RemoveChild(child); + } + } + UpdateServerInfo(serverInfo); SortList(sortedBy, toggle: false); @@ -714,7 +1567,11 @@ namespace Barotrauma private void UpdateServerInfo(ServerInfo serverInfo) { - var serverFrame = serverList.Content.FindChild(serverInfo); + var serverFrame = serverList.Content.FindChild(d => (d.UserData is ServerInfo info) && + (info.LobbyID == serverInfo.LobbyID || + (info.LobbyID == 0 && info.OwnerID == serverInfo.OwnerID && + serverInfo.OwnerVerified)) && + (serverInfo.OwnerID != 0 ? true : (info.IP == serverInfo.IP && info.Port == serverInfo.Port))); if (serverFrame == null) return; var serverContent = serverFrame.Children.First() as GUILayoutGroup; @@ -816,6 +1673,12 @@ namespace Barotrauma } serverContent.Recalculate(); + + if (serverInfo.Favorite) + { + AddToFavoriteServers(serverInfo); + } + SortList(sortedBy, toggle: false); FilterServers(); } @@ -919,7 +1782,7 @@ namespace Barotrauma masterServerResponded = true; } - private bool JoinServer(string ip, string serverName) + private bool JoinServer(string endpoint, string serverName) { if (string.IsNullOrWhiteSpace(clientNameBox.Text)) { @@ -930,7 +1793,7 @@ namespace Barotrauma GameMain.Config.PlayerName = clientNameBox.Text; GameMain.Config.SaveNewPlayerConfig(); - CoroutineManager.StartCoroutine(ConnectToServer(ip, serverName)); + CoroutineManager.StartCoroutine(ConnectToServer(endpoint, serverName)); return true; } @@ -945,7 +1808,7 @@ namespace Barotrauma try { #endif - GameMain.Client = new GameClient(clientNameBox.Text, serverIP, serverSteamID, serverName); + GameMain.Client = new GameClient(GameMain.Config.PlayerName, serverIP, serverSteamID, serverName); #if !DEBUG } catch (Exception e) @@ -959,6 +1822,9 @@ namespace Barotrauma public void GetServerPing(ServerInfo serverInfo, GUITextBlock serverPingText) { + if (activePings.Contains(serverInfo.IP)) { return; } + activePings.Add(serverInfo.IP); + serverInfo.PingChecked = false; serverInfo.Ping = -1; @@ -983,6 +1849,7 @@ namespace Barotrauma serverPingText.TextColor = GetPingTextColor(serverInfo.Ping); } serverPingText.Text = serverInfo.Ping > -1 ? serverInfo.Ping.ToString() : "?"; + activePings.Remove(serverInfo.IP); yield return CoroutineStatus.Success; } @@ -1055,7 +1922,7 @@ namespace Barotrauma GameMain.TitleScreen.DrawLoadingText = false; GameMain.MainMenuScreen.DrawBackground(graphics, spriteBatch); - spriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, GameMain.ScissorTestEnable); + spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); GUI.Draw(Cam, spriteBatch); @@ -1065,6 +1932,10 @@ namespace Barotrauma public override void AddToGUIUpdateList() { menu.AddToGUIUpdateList(); + + friendPopup?.AddToGUIUpdateList(); + + friendsDropdown?.AddToGUIUpdateList(); } } diff --git a/Barotrauma/BarotraumaClient/Source/Screens/SpriteEditorScreen.cs b/Barotrauma/BarotraumaClient/Source/Screens/SpriteEditorScreen.cs index 8cbafe767..0049c791f 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/SpriteEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/SpriteEditorScreen.cs @@ -332,7 +332,7 @@ namespace Barotrauma string spriteFolder = ""; string textureElement = element.GetAttributeString("texture", ""); // TODO: parse and create? - if (textureElement.Contains("[GENDER]") || textureElement.Contains("[HEADID]") || textureElement.Contains("[RACE]")) { return; } + if (textureElement.Contains("[GENDER]") || textureElement.Contains("[HEADID]") || textureElement.Contains("[RACE]") || textureElement.Contains("[VARIANT]")) { return; } if (!textureElement.Contains("/")) { var parsedPath = element.ParseContentPathFromUri(); @@ -428,42 +428,23 @@ namespace Barotrauma viewAreaOffset += moveSpeed.ToPoint(); } } - if (PlayerInput.KeyHit(Keys.Left)) + if (GUI.KeyboardDispatcher.Subscriber == null) { - foreach (var sprite in selectedSprites) + Point moveAmount = Point.Zero; + if (PlayerInput.KeyHit(Keys.Left)) { moveAmount.X--; } + if (PlayerInput.KeyHit(Keys.Right)) { moveAmount.X++; } + if (PlayerInput.KeyHit(Keys.Up)) { moveAmount.Y--; } + if (PlayerInput.KeyHit(Keys.Down)) { moveAmount.Y++; } + if (moveAmount != Point.Zero) { - var newRect = sprite.SourceRect; - newRect.X--; - UpdateSourceRect(sprite, newRect); + foreach (var sprite in selectedSprites) + { + var newRect = sprite.SourceRect; + newRect.Location += moveAmount; + UpdateSourceRect(sprite, newRect); + } } - } - if (PlayerInput.KeyHit(Keys.Right)) - { - foreach (var sprite in selectedSprites) - { - var newRect = sprite.SourceRect; - newRect.X++; - UpdateSourceRect(sprite, newRect); - } - } - if (PlayerInput.KeyHit(Keys.Down)) - { - foreach (var sprite in selectedSprites) - { - var newRect = sprite.SourceRect; - newRect.Y++; - UpdateSourceRect(sprite, newRect); - } - } - if (PlayerInput.KeyHit(Keys.Up)) - { - foreach (var sprite in selectedSprites) - { - var newRect = sprite.SourceRect; - newRect.Y--; - UpdateSourceRect(sprite, newRect); - } - } + } } public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) diff --git a/Barotrauma/BarotraumaClient/Source/Screens/SteamWorkshopScreen.cs b/Barotrauma/BarotraumaClient/Source/Screens/SteamWorkshopScreen.cs index f095bc0c9..2e108884e 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/SteamWorkshopScreen.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/SteamWorkshopScreen.cs @@ -1492,7 +1492,7 @@ namespace Barotrauma else { string errorMsg = item.ErrorCode.HasValue ? - TextManager.Get("WorkshopPublishError." + item.ErrorCode.Value.ToString(), returnNull: true) : + TextManager.GetWithVariable("WorkshopPublishError." + item.ErrorCode.Value.ToString(), "[savepath]", SaveUtil.SaveFolder, returnNull: true) : null; if (errorMsg == null) @@ -1520,7 +1520,7 @@ namespace Barotrauma GameMain.MainMenuScreen.DrawBackground(graphics, spriteBatch); - spriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, GameMain.ScissorTestEnable); + spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); GUI.Draw(Cam, spriteBatch); spriteBatch.End(); } diff --git a/Barotrauma/BarotraumaClient/Source/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/Source/Screens/SubEditorScreen.cs index 941cbf389..c33ff80a0 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/SubEditorScreen.cs @@ -87,6 +87,13 @@ namespace Barotrauma return (Submarine.MainSub == null) ? "" : Submarine.MainSub.Name; } + public string GetSubDescription() + { + string localizedDescription = TextManager.Get("submarine.description." + GetSubName(), true); + if (localizedDescription != null) return localizedDescription; + return (Submarine.MainSub == null) ? "" : Submarine.MainSub.Description; + } + private string GetItemCount() { return TextManager.AddPunctuation(':', TextManager.Get("Items"), Item.ItemList.Count.ToString()); @@ -1048,8 +1055,10 @@ namespace Barotrauma submarineDescriptionCharacterCount = new GUITextBlock(new RectTransform(new Vector2(.5f, 1f), descriptionHeaderGroup.RectTransform), string.Empty, textAlignment: Alignment.TopRight); var descriptionContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.25f), leftColumn.RectTransform)); - descriptionBox = new GUITextBox(new RectTransform(Vector2.One, descriptionContainer.Content.RectTransform, Anchor.Center), font: GUI.SmallFont, wrap: true, textAlignment: Alignment.TopLeft); - descriptionBox.Padding = new Vector4(10 * GUI.Scale); + descriptionBox = new GUITextBox(new RectTransform(Vector2.One, descriptionContainer.Content.RectTransform, Anchor.Center), font: GUI.SmallFont, wrap: true, textAlignment: Alignment.TopLeft) + { + Padding = new Vector4(10 * GUI.Scale) + }; descriptionBox.OnTextChanged += (textBox, text) => { @@ -1068,6 +1077,8 @@ namespace Barotrauma return true; }; + descriptionBox.Text = GetSubDescription(); + var crewSizeArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.03f), leftColumn.RectTransform), isHorizontal: true) { AbsoluteSpacing = 5 }; new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), crewSizeArea.RectTransform), @@ -2478,7 +2489,7 @@ namespace Barotrauma //-------------------- HUD ----------------------------- - spriteBatch.Begin(SpriteSortMode.Deferred); + spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState); if (Submarine.MainSub != null) { diff --git a/Barotrauma/BarotraumaClient/Source/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/Source/Serialization/SerializableEntityEditor.cs index 398d852a4..8ff377982 100644 --- a/Barotrauma/BarotraumaClient/Source/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/Source/Serialization/SerializableEntityEditor.cs @@ -808,16 +808,19 @@ namespace Barotrauma AbsoluteOffset = new Point(label.Rect.Width, 0) }, color: Color.Black, style: null); var colorBox = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), colorBoxBack.RectTransform, Anchor.Center), style: null); - var inputArea = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 1), frame.RectTransform, Anchor.TopRight), isHorizontal: true, childAnchor: Anchor.CenterRight) + var inputArea = new GUILayoutGroup(new RectTransform(new Vector2(Math.Max((frame.Rect.Width - label.Rect.Width - colorBoxBack.Rect.Width) / (float)frame.Rect.Width, 0.5f), 1), frame.RectTransform, Anchor.TopRight), isHorizontal: true, childAnchor: Anchor.CenterRight) { Stretch = true, - RelativeSpacing = 0.01f + RelativeSpacing = 0.001f }; var fields = new GUIComponent[4]; for (int i = 3; i >= 0; i--) { - var element = new GUIFrame(new RectTransform(new Vector2(0.2f, 1), inputArea.RectTransform) { MinSize = new Point(40, 0), MaxSize = new Point(100, 50) }, style: null); - new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), GUI.colorComponentLabels[i], font: GUI.SmallFont, textAlignment: Alignment.CenterLeft); + var element = new GUILayoutGroup(new RectTransform(new Vector2(0.2f, 1), inputArea.RectTransform), isHorizontal: true) + { + Stretch = true + }; + new GUITextBlock(new RectTransform(new Vector2(0.2f, 1), element.RectTransform, Anchor.CenterLeft) { MinSize = new Point(15, 0) }, GUI.colorComponentLabels[i], font: GUI.SmallFont, textAlignment: Alignment.Center); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), GUINumberInput.NumberType.Int) { diff --git a/Barotrauma/BarotraumaClient/Source/Sounds/SoundChannel.cs b/Barotrauma/BarotraumaClient/Source/Sounds/SoundChannel.cs index ab9bc1a0c..8b9cc129b 100644 --- a/Barotrauma/BarotraumaClient/Source/Sounds/SoundChannel.cs +++ b/Barotrauma/BarotraumaClient/Source/Sounds/SoundChannel.cs @@ -83,6 +83,8 @@ namespace Barotrauma.Sounds private const int STREAM_BUFFER_SIZE = 8820; private short[] streamShortBuffer; + private string debugName = "SoundChannel"; + private Vector3? position; public Vector3? Position { @@ -91,7 +93,7 @@ namespace Barotrauma.Sounds { position = value; - if (ALSourceIndex < 0) return; + if (ALSourceIndex < 0) { return; } if (position != null) { @@ -100,14 +102,14 @@ namespace Barotrauma.Sounds int alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to enable source's relative flag: " + Al.GetErrorString(alError)); + throw new Exception("Failed to enable source's relative flag: " + debugName + ", " + Al.GetErrorString(alError)); } Al.Source3f(alSource, Al.Position, position.Value.X, position.Value.Y, position.Value.Z); alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to set source's position: " + Al.GetErrorString(alError)); + throw new Exception("Failed to set source's position: " + debugName + ", " + Al.GetErrorString(alError)); } } else @@ -117,14 +119,14 @@ namespace Barotrauma.Sounds int alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to disable source's relative flag: " + Al.GetErrorString(alError)); + throw new Exception("Failed to disable source's relative flag: " + debugName + ", " + Al.GetErrorString(alError)); } Al.Source3f(alSource, Al.Position, 0.0f, 0.0f, 0.0f); alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to reset source's position: " + Al.GetErrorString(alError)); + throw new Exception("Failed to reset source's position: " + debugName + ", " + Al.GetErrorString(alError)); } } } @@ -138,7 +140,7 @@ namespace Barotrauma.Sounds { near = value; - if (ALSourceIndex < 0) return; + if (ALSourceIndex < 0) { return; } uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); Al.Sourcef(alSource, Al.ReferenceDistance, near); @@ -146,7 +148,7 @@ namespace Barotrauma.Sounds int alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to set source's reference distance: " + Al.GetErrorString(alError)); + throw new Exception("Failed to set source's reference distance: " + debugName + ", " + Al.GetErrorString(alError)); } } } @@ -159,14 +161,14 @@ namespace Barotrauma.Sounds { far = value; - if (ALSourceIndex < 0) return; + if (ALSourceIndex < 0) { return; } uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); Al.Sourcef(alSource, Al.MaxDistance, far); int alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to set source's max distance: " + Al.GetErrorString(alError)); + throw new Exception("Failed to set source's max distance: " + debugName + ", " + Al.GetErrorString(alError)); } } } @@ -179,7 +181,7 @@ namespace Barotrauma.Sounds { gain = Math.Max(Math.Min(value, 1.0f), 0.0f); - if (ALSourceIndex < 0) return; + if (ALSourceIndex < 0) { return; } uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); @@ -190,7 +192,7 @@ namespace Barotrauma.Sounds int alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to set source's gain: " + Al.GetErrorString(alError)); + throw new Exception("Failed to set source's gain: " + debugName + ", " + Al.GetErrorString(alError)); } } } @@ -203,7 +205,7 @@ namespace Barotrauma.Sounds { looping = value; - if (ALSourceIndex < 0) return; + if (ALSourceIndex < 0) { return; } if (!IsStream) { @@ -212,7 +214,7 @@ namespace Barotrauma.Sounds int alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to set source's looping state: " + Al.GetErrorString(alError)); + throw new Exception("Failed to set source's looping state: " + debugName + ", " + Al.GetErrorString(alError)); } } } @@ -232,11 +234,11 @@ namespace Barotrauma.Sounds get { return muffled; } set { - if (muffled == value) return; + if (muffled == value) { return; } muffled = value; - if (ALSourceIndex < 0) return; + if (ALSourceIndex < 0) { return; } if (!IsPlaying) return; @@ -247,7 +249,7 @@ namespace Barotrauma.Sounds int alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to get source's playback position: " + Al.GetErrorString(alError)); + throw new Exception("Failed to get source's playback position: " + debugName + ", " + Al.GetErrorString(alError)); } Al.SourceStop(alSource); @@ -255,7 +257,7 @@ namespace Barotrauma.Sounds alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to stop source: " + Al.GetErrorString(alError)); + throw new Exception("Failed to stop source: " + debugName + ", " + Al.GetErrorString(alError)); } Al.Sourcei(alSource, Al.Buffer, muffled ? (int)Sound.ALMuffledBuffer : (int)Sound.ALBuffer); @@ -263,21 +265,21 @@ namespace Barotrauma.Sounds alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to bind buffer to source: " + Al.GetErrorString(alError)); + throw new Exception("Failed to bind buffer to source: " + debugName + ", " + Al.GetErrorString(alError)); } Al.SourcePlay(alSource); alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to replay source: " + Al.GetErrorString(alError)); + throw new Exception("Failed to replay source: " + debugName + ", " + Al.GetErrorString(alError)); } Al.Sourcei(alSource, Al.SampleOffset, playbackPos); alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to reset playback position: " + Al.GetErrorString(alError)); + throw new Exception("Failed to reset playback position: " + debugName + ", " + Al.GetErrorString(alError)); } } } @@ -290,7 +292,7 @@ namespace Barotrauma.Sounds { if (!IsPlaying) { return 0.0f; } - uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); + uint alSource = Sound?.Owner?.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex) ?? 0; if (alSource == 0) { return 0.0f; } @@ -300,7 +302,7 @@ namespace Barotrauma.Sounds int alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to get source's playback position: " + Al.GetErrorString(alError)); + throw new Exception("Failed to get source's playback position: " + debugName + ", " + Al.GetErrorString(alError)); } return Sound.GetAmplitudeAtPlaybackPos(playbackPos); } @@ -367,7 +369,7 @@ namespace Barotrauma.Sounds int alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to determine playing state from source: " + Al.GetErrorString(alError)); + throw new Exception("Failed to determine playing state from source: " + debugName + ", " + Al.GetErrorString(alError)); } return playing; } @@ -377,6 +379,10 @@ namespace Barotrauma.Sounds { Sound = sound; + debugName = sound == null ? + "SoundChannel (null)" : + $"SoundChannel ({(string.IsNullOrEmpty(sound.Filename) ? "filename empty" : sound.Filename) })"; + IsStream = sound.Stream; FilledByNetwork = sound is VoipSound; decayTimer = 0; @@ -405,7 +411,7 @@ namespace Barotrauma.Sounds int alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to reset source buffer: " + Al.GetErrorString(alError)); + throw new Exception("Failed to reset source buffer: " + debugName + ", " + Al.GetErrorString(alError)); } if (!Al.IsBuffer(sound.ALBuffer)) @@ -418,14 +424,14 @@ namespace Barotrauma.Sounds alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to bind buffer to source (" + ALSourceIndex.ToString() + ":" + sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex) + "," + sound.ALBuffer.ToString() + "): " + Al.GetErrorString(alError)); + throw new Exception("Failed to bind buffer to source (" + ALSourceIndex.ToString() + ":" + sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex) + "," + sound.ALBuffer.ToString() + "): " + debugName + ", " + Al.GetErrorString(alError)); } Al.SourcePlay(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex)); alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to play source: " + Al.GetErrorString(alError)); + throw new Exception("Failed to play source: " + debugName + ", " + Al.GetErrorString(alError)); } } else @@ -435,14 +441,14 @@ namespace Barotrauma.Sounds int alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to reset source buffer: " + Al.GetErrorString(alError)); + throw new Exception("Failed to reset source buffer: " + debugName + ", " + Al.GetErrorString(alError)); } Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Looping, Al.False); alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to set stream looping state: " + Al.GetErrorString(alError)); + throw new Exception("Failed to set stream looping state: " + debugName + ", " + Al.GetErrorString(alError)); } streamShortBuffer = new short[STREAM_BUFFER_SIZE]; @@ -457,12 +463,12 @@ namespace Barotrauma.Sounds alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to generate stream buffers: " + Al.GetErrorString(alError)); + throw new Exception("Failed to generate stream buffers: " + debugName + ", " + Al.GetErrorString(alError)); } if (!Al.IsBuffer(streamBuffers[i])) { - throw new Exception("Generated streamBuffer[" + i.ToString() + "] is invalid!"); + throw new Exception("Generated streamBuffer[" + i.ToString() + "] is invalid! " + debugName); } } Sound.Owner.InitStreamThread(); @@ -488,6 +494,11 @@ namespace Barotrauma.Sounds Sound.Owner.Update(); } + public override string ToString() + { + return debugName; + } + public bool FadingOutAndDisposing; public void FadeOutAndDispose() { @@ -505,7 +516,7 @@ namespace Barotrauma.Sounds int alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to stop source: " + Al.GetErrorString(alError)); + throw new Exception("Failed to stop source: " + debugName + ", " + Al.GetErrorString(alError)); } if (IsStream) @@ -516,7 +527,7 @@ namespace Barotrauma.Sounds alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to stop streamed source: " + Al.GetErrorString(alError)); + throw new Exception("Failed to stop streamed source: " + debugName + ", " + Al.GetErrorString(alError)); } int buffersToRequeue = 0; @@ -526,21 +537,21 @@ namespace Barotrauma.Sounds alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to determine processed buffers from streamed source: " + Al.GetErrorString(alError)); + throw new Exception("Failed to determine processed buffers from streamed source: " + debugName + ", " + Al.GetErrorString(alError)); } Al.SourceUnqueueBuffers(alSource, buffersToRequeue, unqueuedBuffers); alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to unqueue buffers from streamed source: " + Al.GetErrorString(alError)); + throw new Exception("Failed to unqueue buffers from streamed source: " + debugName + ", " + Al.GetErrorString(alError)); } Al.Sourcei(alSource, Al.Buffer, 0); alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to reset buffer for streamed source: " + Al.GetErrorString(alError)); + throw new Exception("Failed to reset buffer for streamed source: " + debugName + ", " + Al.GetErrorString(alError)); } for (int i = 0; i < 4; i++) @@ -549,7 +560,7 @@ namespace Barotrauma.Sounds alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to delete streamBuffers[" + i.ToString() + "] ("+streamBuffers[i].ToString()+"): " + Al.GetErrorString(alError)); + throw new Exception("Failed to delete streamBuffers[" + i.ToString() + "] (" + streamBuffers[i].ToString() + "): " + debugName + ", " + Al.GetErrorString(alError)); } } @@ -561,11 +572,12 @@ namespace Barotrauma.Sounds alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to unbind buffer to non-streamed source: " + Al.GetErrorString(alError)); + throw new Exception("Failed to unbind buffer to non-streamed source: " + debugName + ", " + Al.GetErrorString(alError)); } } ALSourceIndex = -1; + debugName += " [DISPOSED]"; } } finally @@ -576,7 +588,7 @@ namespace Barotrauma.Sounds public void UpdateStream() { - if (!IsStream) throw new Exception("Called UpdateStream on a non-streamed sound channel!"); + if (!IsStream) { throw new Exception("Called UpdateStream on a non-streamed sound channel!"); } try { @@ -591,7 +603,7 @@ namespace Barotrauma.Sounds int alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to determine playing state from streamed source: " + Al.GetErrorString(alError)); + throw new Exception("Failed to determine playing state from streamed source: " + debugName + ", " + Al.GetErrorString(alError)); } int unqueuedBufferCount; @@ -599,16 +611,16 @@ namespace Barotrauma.Sounds alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to determine processed buffers from streamed source: " + Al.GetErrorString(alError)); + throw new Exception("Failed to determine processed buffers from streamed source: " + debugName + ", " + Al.GetErrorString(alError)); } Al.SourceUnqueueBuffers(alSource, unqueuedBufferCount, unqueuedBuffers); alError = Al.GetError(); if (alError != Al.NoError) { - throw new Exception("Failed to unqueue buffers from streamed source: " + Al.GetErrorString(alError)); + throw new Exception("Failed to unqueue buffers from streamed source: " + debugName + ", " + Al.GetErrorString(alError)); } - + buffersToRequeue += unqueuedBufferCount; int iterCount = buffersToRequeue; @@ -619,7 +631,7 @@ namespace Barotrauma.Sounds int readSamples = Sound.FillStreamBuffer(streamSeekPos, buffer); float readAmplitude = 0.0f; - for (int i=0;i loadedSounds; + private readonly List loadedSounds; private readonly SoundChannel[][] playingChannels = new SoundChannel[2][]; private Thread streamingThread; @@ -51,7 +51,7 @@ namespace Barotrauma.Sounds } } - private float[] listenerOrientation = new float[6]; + private readonly float[] listenerOrientation = new float[6]; public Vector3 ListenerTargetVector { get { return new Vector3(listenerOrientation[0], listenerOrientation[1], listenerOrientation[2]); } @@ -245,8 +245,6 @@ namespace Barotrauma.Sounds return; } - int alError = Al.NoError; - sourcePools = new SoundSourcePool[2]; sourcePools[(int)SourcePoolIndex.Default] = new SoundSourcePool(SOURCE_COUNT); playingChannels[(int)SourcePoolIndex.Default] = new SoundChannel[SOURCE_COUNT]; @@ -256,7 +254,7 @@ namespace Barotrauma.Sounds Al.DistanceModel(Al.LinearDistanceClamped); - alError = Al.GetError(); + int alError = Al.GetError(); if (alError != Al.NoError) { DebugConsole.ThrowError("Error setting distance model: " + Al.GetErrorString(alError) + ". Disabling audio playback..."); @@ -488,7 +486,7 @@ namespace Barotrauma.Sounds if (index < 0) { float accumulatedMultipliers = 1.0f; - for (int i=0;i targets, Hull hull) + partial void ApplyProjSpecific(float deltaTime, Entity entity, List targets, Hull hull, Vector2 worldPosition) { if (entity == null) { return; } @@ -70,7 +70,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("StatusEffect.ApplyProjSpecific:SoundNull1" + Environment.StackTrace, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); return; } - soundChannel = SoundPlayer.PlaySound(sound.Sound, entity.WorldPosition, sound.Volume, sound.Range, hull); + soundChannel = SoundPlayer.PlaySound(sound.Sound, worldPosition, sound.Volume, sound.Range, hull); if (soundChannel != null) { soundChannel.Looping = loopSound; } } } @@ -96,7 +96,7 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("StatusEffect.ApplyProjSpecific:SoundNull2" + Environment.StackTrace, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); return; } - soundChannel = SoundPlayer.PlaySound(selectedSound.Sound, entity.WorldPosition, selectedSound.Volume, selectedSound.Range, hull); + soundChannel = SoundPlayer.PlaySound(selectedSound.Sound, worldPosition, selectedSound.Volume, selectedSound.Range, hull); if (soundChannel != null) { soundChannel.Looping = loopSound; } } } @@ -120,7 +120,7 @@ namespace Barotrauma } } - emitter.Emit(deltaTime, entity.WorldPosition, hull, angle); + emitter.Emit(deltaTime, worldPosition, hull, angle); } } diff --git a/Barotrauma/BarotraumaClient/Source/Utils/TextureLoader.cs b/Barotrauma/BarotraumaClient/Source/Utils/TextureLoader.cs index 20dd90061..bb27f7672 100644 --- a/Barotrauma/BarotraumaClient/Source/Utils/TextureLoader.cs +++ b/Barotrauma/BarotraumaClient/Source/Utils/TextureLoader.cs @@ -57,13 +57,13 @@ namespace Barotrauma }); } - public static Texture2D FromFile(string path, bool preMultiplyAlpha = true) + public static Texture2D FromFile(string path, bool preMultiplyAlpha = true, bool mipmap=false) { try { using (Stream fileStream = File.OpenRead(path)) { - return FromStream(fileStream, preMultiplyAlpha, path); + return FromStream(fileStream, preMultiplyAlpha, path, mipmap); } } @@ -74,7 +74,7 @@ namespace Barotrauma } } - public static Texture2D FromStream(Stream fileStream, bool preMultiplyAlpha = true, string path=null) + public static Texture2D FromStream(Stream fileStream, bool preMultiplyAlpha = true, string path=null, bool mipmap=false) { try { @@ -88,7 +88,7 @@ namespace Barotrauma Texture2D tex = null; CrossThread.RequestExecutionOnMainThread(() => { - tex = new Texture2D(_graphicsDevice, width, height); + tex = new Texture2D(_graphicsDevice, width, height, mipmap, SurfaceFormat.Color); tex.SetData(textureData); }); return tex; diff --git a/Barotrauma/BarotraumaServer/Properties/AssemblyInfo.cs b/Barotrauma/BarotraumaServer/Properties/AssemblyInfo.cs index 862c08bd2..bd05b4084 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.9.4.0")] -[assembly: AssemblyFileVersion("0.9.4.0")] +[assembly: AssemblyVersion("0.9.5.1")] +[assembly: AssemblyFileVersion("0.9.5.1")] diff --git a/Barotrauma/BarotraumaServer/Server.csproj b/Barotrauma/BarotraumaServer/Server.csproj index 98cdb5a19..c037a2411 100644 --- a/Barotrauma/BarotraumaServer/Server.csproj +++ b/Barotrauma/BarotraumaServer/Server.csproj @@ -232,6 +232,10 @@ + + + + @@ -310,11 +314,7 @@ - - - - - + diff --git a/Barotrauma/BarotraumaServer/Source/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaServer/Source/Characters/CharacterInfo.cs index 467a4fe13..a9d2205c5 100644 --- a/Barotrauma/BarotraumaServer/Source/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/Source/Characters/CharacterInfo.cs @@ -20,6 +20,7 @@ namespace Barotrauma if (Job != null) { msg.Write(Job.Prefab.Identifier); + msg.Write((byte)Job.Variant); msg.Write((byte)Job.Skills.Count); foreach (Skill skill in Job.Skills) { @@ -30,6 +31,7 @@ namespace Barotrauma else { msg.Write(""); + msg.Write((byte)0); } // TODO: animations } diff --git a/Barotrauma/BarotraumaServer/Source/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/Source/Characters/CharacterNetworking.cs index 2dc77d5da..a01d48706 100644 --- a/Barotrauma/BarotraumaServer/Source/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/Source/Characters/CharacterNetworking.cs @@ -45,7 +45,7 @@ namespace Barotrauma { if (!(this is AICharacter) || IsRemotePlayer) { - if (!AllowInput) + if (!CanMove) { AnimController.Frozen = false; if (memInput.Count > 0) @@ -156,7 +156,7 @@ namespace Barotrauma UInt16 networkUpdateID = msg.ReadUInt16(); byte inputCount = msg.ReadByte(); - if (AllowInput) Enabled = true; + if (AllowInput) { Enabled = true; } for (int i = 0; i < inputCount; i++) { @@ -470,7 +470,11 @@ namespace Barotrauma msg.Write(Enabled); //character with no characterinfo (e.g. some monster) - if (Info == null) return; + if (Info == null) + { + WriteStatus(msg); + return; + } Client ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == this); if (ownerClient != null) @@ -492,6 +496,7 @@ namespace Barotrauma msg.Write(this is AICharacter); msg.Write(info.SpeciesName); info.ServerWrite(msg); + WriteStatus(msg); DebugConsole.Log("Character spawn message length: " + (msg.LengthBytes - msgLength)); } diff --git a/Barotrauma/BarotraumaServer/Source/DebugConsole.cs b/Barotrauma/BarotraumaServer/Source/DebugConsole.cs index cb64ed2fb..1a58f9927 100644 --- a/Barotrauma/BarotraumaServer/Source/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/Source/DebugConsole.cs @@ -197,7 +197,20 @@ namespace Barotrauma } break; default: - if (key.KeyChar != 0) + if (key.Modifiers.HasFlag(ConsoleModifiers.Control)) + { + if (key.Key == ConsoleKey.Z) + { + activeQuestionCallback = null; + NewMessage("^Z"); + } + else if (key.Key == ConsoleKey.D) + { + activeQuestionCallback = null; + NewMessage("^D"); + } + } + else if (key.KeyChar != 0) { input += key.KeyChar; memoryIndex = -1; @@ -759,10 +772,12 @@ namespace Barotrauma { if (GameMain.Server == null || args.Length == 0) return; - ShowQuestionPrompt("Reason for banning the endpoint \"" + args[0] + "\"?", (reason) => + ShowQuestionPrompt("Reason for banning the endpoint \"" + args[0] + "\"? (c to cancel)", (reason) => { - ShowQuestionPrompt("Enter the duration of the ban (leave empty to ban permanently, or use the format \"[days] d [hours] h\")", (duration) => + if (reason == "c" || reason == "C") { return; } + ShowQuestionPrompt("Enter the duration of the ban (leave empty to ban permanently, or use the format \"[days] d [hours] h\") (c to cancel)", (duration) => { + if (duration == "c" || duration == "C") { return; } TimeSpan? banDuration = null; if (!string.IsNullOrWhiteSpace(duration)) { @@ -859,6 +874,11 @@ namespace Barotrauma client.SpectateOnly = false; }); + AssignOnExecute("starttraitormissionimmediately", (string[] args) => + { + GameMain.Server?.TraitorManager?.SkipStartDelay(); + }); + AssignOnExecute("difficulty|leveldifficulty", (string[] args) => { if (GameMain.Server == null || args.Length < 1) return; @@ -1140,14 +1160,7 @@ namespace Barotrauma commands.Add(new Command("mission", "mission [name]/[index]: Select the mission type for the next round. The parameter can either be the name or the index number of the mission type (0 = first mission type, 1 = second mission type, etc).", (string[] args) => { int index = -1; - if (int.TryParse(string.Join(" ", args), out index)) - { - GameMain.NetLobbyScreen.MissionTypeIndex = index; - } - else - { - GameMain.NetLobbyScreen.MissionTypeName = string.Join(" ", args); - } + GameMain.NetLobbyScreen.MissionTypeName = string.Join(" ", args); NewMessage("Set mission to " + GameMain.NetLobbyScreen.MissionTypeName, Color.Cyan); }, () => diff --git a/Barotrauma/BarotraumaServer/Source/GameMain.cs b/Barotrauma/BarotraumaServer/Source/GameMain.cs index bea654413..7f337c4ab 100644 --- a/Barotrauma/BarotraumaServer/Source/GameMain.cs +++ b/Barotrauma/BarotraumaServer/Source/GameMain.cs @@ -243,6 +243,18 @@ namespace Barotrauma maxPlayers, ownerKey, steamId); + + for (int i = 0; i < CommandLineArgs.Length; i++) + { + switch (CommandLineArgs[i].Trim()) + { + case "-playstyle": + Enum.TryParse(CommandLineArgs[i + 1], out PlayStyle playStyle); + Server.ServerSettings.PlayStyle = playStyle; + i++; + break; + } + } } public void CloseServer() diff --git a/Barotrauma/BarotraumaServer/Source/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaServer/Source/Items/Components/Machines/Steering.cs index 28b81c2af..7ff0d9ea4 100644 --- a/Barotrauma/BarotraumaServer/Source/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaServer/Source/Items/Components/Machines/Steering.cs @@ -5,6 +5,7 @@ namespace Barotrauma.Items.Components { partial class Steering : Powered, IServerSerializable, IClientSerializable { + // TODO: an enumeration would be much cleaner public bool MaintainPos; public bool LevelStartSelected; public bool LevelEndSelected; diff --git a/Barotrauma/BarotraumaServer/Source/Map/Hull.cs b/Barotrauma/BarotraumaServer/Source/Map/Hull.cs index dd3b8de39..6300da769 100644 --- a/Barotrauma/BarotraumaServer/Source/Map/Hull.cs +++ b/Barotrauma/BarotraumaServer/Source/Map/Hull.cs @@ -29,14 +29,15 @@ namespace Barotrauma return; } + sendUpdateTimer -= deltaTime; //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 || Math.Abs(lastSentOxygen - OxygenPercentage) > 5f || lastSentFireCount != FireSources.Count || - FireSources.Count > 0) + FireSources.Count > 0 || + sendUpdateTimer < -NetConfig.SparseHullUpdateInterval) { - sendUpdateTimer -= deltaTime; if (sendUpdateTimer < 0.0f) { GameMain.NetworkMember.CreateEntityEvent(this); diff --git a/Barotrauma/BarotraumaServer/Source/Networking/BanList.cs b/Barotrauma/BarotraumaServer/Source/Networking/BanList.cs index 2e089edc2..929274fe0 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/BanList.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/BanList.cs @@ -52,7 +52,7 @@ namespace Barotrauma.Networking public bool CompareTo(IPAddress ipCompare) { if (string.IsNullOrEmpty(IP) || ipCompare == null) { return false; } - if (ipCompare.IsIPv4MappedToIPv6 && CompareTo(ipCompare.MapToIPv4().ToString())) + if (ipCompare.IsIPv4MappedToIPv6 && CompareTo(ipCompare.MapToIPv4NoThrow().ToString())) { return true; } @@ -138,7 +138,7 @@ namespace Barotrauma.Networking public void BanPlayer(string name, IPAddress ip, string reason, TimeSpan? duration) { - string ipStr = ip.IsIPv4MappedToIPv6 ? ip.MapToIPv4().ToString() : ip.ToString(); + string ipStr = ip.IsIPv4MappedToIPv6 ? ip.MapToIPv4NoThrow().ToString() : ip.ToString(); BanPlayer(name, ipStr, 0, reason, duration); } diff --git a/Barotrauma/BarotraumaServer/Source/Networking/Client.cs b/Barotrauma/BarotraumaServer/Source/Networking/Client.cs index ee4663238..e7600d7e1 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/Client.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/Client.cs @@ -32,6 +32,8 @@ namespace Barotrauma.Networking public float ChatSpamTimer; public int ChatSpamCount; + public int RoundsSincePlayedAsTraitor; + public float KickAFKTimer; public double MidRoundSyncTimeOut; @@ -52,12 +54,22 @@ namespace Barotrauma.Networking public bool ReadyToStart; - public List JobPreferences; - public JobPrefab AssignedJob; + public List> JobPreferences; + public Pair AssignedJob; public float DeleteDisconnectedTimer; - public CharacterInfo CharacterInfo; + private CharacterInfo characterInfo; + public CharacterInfo CharacterInfo + { + get { return characterInfo; } + set + { + if (characterInfo == value) { return; } + characterInfo?.Remove(); + characterInfo = value; + } + } public NetworkConnection Connection { get; set; } public bool SpectateOnly; @@ -84,7 +96,7 @@ namespace Barotrauma.Networking { var jobs = JobPrefab.List.Values.ToList(); // TODO: modding support? - JobPreferences = new List(jobs.GetRange(0, Math.Min(jobs.Count, 3))); + JobPreferences = new List>(jobs.GetRange(0, Math.Min(jobs.Count, 3)).Select(j => new Pair(j, 0))); VoipQueue = new VoipQueue(ID, true, true); GameMain.Server.VoipServer.RegisterQueue(VoipQueue); @@ -94,6 +106,8 @@ namespace Barotrauma.Networking { GameMain.Server.VoipServer.UnregisterQueue(VoipQueue); VoipQueue.Dispose(); + characterInfo?.Remove(); + characterInfo = null; } public void InitClientSync() @@ -128,7 +142,7 @@ namespace Barotrauma.Networking { if (lidgrenConn.IPEndPoint?.Address == null) { return false; } if ((lidgrenConn.IPEndPoint?.Address.IsIPv4MappedToIPv6 ?? false) && - lidgrenConn.IPEndPoint?.Address.MapToIPv4().ToString() == endpoint) + lidgrenConn.IPEndPoint?.Address.MapToIPv4NoThrow().ToString() == endpoint) { return true; } diff --git a/Barotrauma/BarotraumaServer/Source/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/Source/Networking/GameServer.cs index fefda7b32..fd7ed2d95 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/GameServer.cs @@ -13,6 +13,7 @@ using System.IO; using Barotrauma.Steam; using System.Xml.Linq; using System.Threading; +using Barotrauma.Extensions; namespace Barotrauma.Networking { @@ -77,7 +78,7 @@ namespace Barotrauma.Networking public TraitorManager TraitorManager; - private ServerEntityEventManager entityEventManager; + private readonly ServerEntityEventManager entityEventManager; private FileSender fileSender; #if DEBUG @@ -115,8 +116,8 @@ namespace Barotrauma.Networking public int QueryPort => serverSettings?.QueryPort ?? 0; public NetworkConnection OwnerConnection { get; private set; } - private int? ownerKey; - private UInt64? ownerSteamId; + private readonly int? ownerKey; + private readonly UInt64? ownerSteamId; public GameServer(string name, int port, int queryPort = 0, bool isPublic = false, string password = "", bool attemptUPnP = false, int maxPlayers = 10, int? ownKey = null, UInt64? steamId = null) { @@ -215,6 +216,16 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.Select(); GameMain.NetLobbyScreen.RandomizeSettings(); + if (!string.IsNullOrEmpty(serverSettings.SelectedSubmarine)) + { + Submarine sub = Submarine.SavedSubmarines.FirstOrDefault(s => s.Name == serverSettings.SelectedSubmarine); + if (sub != null) { GameMain.NetLobbyScreen.SelectedSub = sub; } + } + if (!string.IsNullOrEmpty(serverSettings.SelectedShuttle)) + { + Submarine shuttle = Submarine.SavedSubmarines.FirstOrDefault(s => s.Name == serverSettings.SelectedShuttle); + if (shuttle != null) { GameMain.NetLobbyScreen.SelectedShuttle = shuttle; } + } started = true; GameAnalyticsManager.AddDesignEvent("GameServer:Start"); @@ -1438,7 +1449,7 @@ namespace Barotrauma.Networking } //no more room in this packet - if (outmsg.LengthBytes + tempBuffer.LengthBytes > MsgConstants.MTU - 20) + if (outmsg.LengthBytes + tempBuffer.LengthBytes > MsgConstants.MTU - 100) { break; } @@ -1523,6 +1534,7 @@ namespace Barotrauma.Networking outmsg.Write(client.SteamID); outmsg.Write(client.NameID); outmsg.Write(client.Name); + outmsg.Write(client.Character == null || !gameStarted ? (client.PreferredJob ?? "") : ""); outmsg.Write(client.Character == null || !gameStarted ? (ushort)0 : client.Character.ID); outmsg.Write(client.Muted); outmsg.Write(client.Connection != OwnerConnection); //is kicking the player allowed @@ -1578,7 +1590,7 @@ namespace Barotrauma.Networking outmsg.WriteRangedInteger((int)serverSettings.TraitorsEnabled, 0, 2); - outmsg.WriteRangedInteger((GameMain.NetLobbyScreen.MissionTypeIndex), 0, Enum.GetValues(typeof(MissionType)).Length - 1); + outmsg.WriteRangedInteger((int)GameMain.NetLobbyScreen.MissionType, 0, (int)MissionType.All); outmsg.Write((byte)GameMain.NetLobbyScreen.SelectedModeIndex); outmsg.Write(GameMain.NetLobbyScreen.LevelSeed); @@ -1807,7 +1819,7 @@ namespace Barotrauma.Networking //don't instantiate a new gamesession if we're playing a campaign if (campaign == null || GameMain.GameSession == null) { - GameMain.GameSession = new GameSession(selectedSub, "", selectedMode, (MissionType)GameMain.NetLobbyScreen.MissionTypeIndex); + GameMain.GameSession = new GameSession(selectedSub, "", selectedMode, GameMain.NetLobbyScreen.MissionType); } List playingClients = new List(connectedClients); @@ -1875,9 +1887,13 @@ namespace Barotrauma.Networking } //find the clients in this team - List teamClients = teamCount == 1 ? - new List(playingClients) : - playingClients.FindAll(c => c.TeamID == teamID); + List teamClients = teamCount == 1 ? new List(playingClients) : playingClients.FindAll(c => c.TeamID == teamID); + if (serverSettings.AllowSpectating) + { + teamClients.RemoveAll(c => c.SpectateOnly); + } + //always allow the server owner to spectate even if it's disallowed in server settings + teamClients.RemoveAll(c => c.Connection == OwnerConnection && c.SpectateOnly); if (!teamClients.Any() && n > 0) { continue; } @@ -1899,9 +1915,9 @@ namespace Barotrauma.Networking client.CharacterInfo = new CharacterInfo(Character.HumanSpeciesName, client.Name); } characterInfos.Add(client.CharacterInfo); - if (client.CharacterInfo.Job == null || client.CharacterInfo.Job.Prefab != client.AssignedJob) + if (client.CharacterInfo.Job == null || client.CharacterInfo.Job.Prefab != client.AssignedJob.First) { - client.CharacterInfo.Job = new Job(client.AssignedJob); + client.CharacterInfo.Job = new Job(client.AssignedJob.First, client.AssignedJob.Second); } } @@ -1909,7 +1925,10 @@ namespace Barotrauma.Networking int botsToSpawn = serverSettings.BotSpawnMode == BotSpawnMode.Fill ? serverSettings.BotCount - characterInfos.Count : serverSettings.BotCount; for (int i = 0; i < botsToSpawn; i++) { - var botInfo = new CharacterInfo(Character.HumanSpeciesName); + var botInfo = new CharacterInfo(Character.HumanSpeciesName) + { + TeamID = teamID + }; characterInfos.Add(botInfo); bots.Add(botInfo); } @@ -2016,7 +2035,7 @@ namespace Barotrauma.Networking msg.Write((byte)GameMain.Config.LosMode); - msg.Write((byte)GameMain.NetLobbyScreen.MissionTypeIndex); + msg.Write((byte)GameMain.NetLobbyScreen.MissionType); msg.Write(selectedSub.Name); msg.Write(selectedSub.MD5Hash.Hash); @@ -2088,7 +2107,7 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.LastUpdateID++; } - if (serverSettings.SaveServerLogs) serverSettings.ServerLog.Save(); + if (serverSettings.SaveServerLogs) { serverSettings.ServerLog.Save(); } GameMain.GameScreen.Cam.TargetPos = Vector2.Zero; @@ -2146,18 +2165,21 @@ namespace Barotrauma.Networking { UInt16 nameId = inc.ReadUInt16(); string newName = inc.ReadString(); + string newJob = inc.ReadString(); if (c == null || string.IsNullOrEmpty(newName) || !NetIdUtils.IdMoreRecent(nameId, c.NameID)) { return false; } c.NameID = nameId; - newName = Client.SanitizeName(newName); - if (newName == c.Name) { return false; } + if (newName == c.Name && newJob == c.PreferredJob) { return false; } + c.PreferredJob = newJob; //update client list even if the name cannot be changed to the one sent by the client, //so the client will be informed what their actual name is LastClientListUpdateID++; + if (newName == c.Name) { return false; } + if (c.Connection != OwnerConnection) { if (!Client.IsValidName(newName, serverSettings)) @@ -2261,7 +2283,7 @@ namespace Barotrauma.Networking if (client.Connection is LidgrenConnection lidgrenConn) { ip = lidgrenConn.IPEndPoint.Address.IsIPv4MappedToIPv6 ? - lidgrenConn.IPEndPoint.Address.MapToIPv4().ToString() : + lidgrenConn.IPEndPoint.Address.MapToIPv4NoThrow().ToString() : lidgrenConn.IPEndPoint.Address.ToString(); if (range) { ip = serverSettings.BanList.ToRange(ip); } } @@ -2896,15 +2918,16 @@ namespace Barotrauma.Networking int moustacheIndex = message.ReadByte(); int faceAttachmentIndex = message.ReadByte(); - List jobPreferences = new List(); + List> jobPreferences = new List>(); int count = message.ReadByte(); // TODO: modding support? for (int i = 0; i < Math.Min(count, 3); i++) { string jobIdentifier = message.ReadString(); + int variant = message.ReadByte(); if (JobPrefab.List.TryGetValue(jobIdentifier, out JobPrefab jobPrefab)) { - jobPreferences.Add(jobPrefab); + jobPreferences.Add(new Pair(jobPrefab, variant)); } } @@ -2923,6 +2946,7 @@ namespace Barotrauma.Networking { var jobList = JobPrefab.List.Values.ToList(); unassigned = new List(unassigned); + unassigned = unassigned.OrderBy(sp => Rand.Int(int.MaxValue)).ToList(); Dictionary assignedClientCount = new Dictionary(); foreach (JobPrefab jp in jobList) @@ -2944,14 +2968,14 @@ namespace Barotrauma.Networking foreach (KeyValuePair clientJob in campaignAssigned) { assignedClientCount[clientJob.Value.Prefab]++; - clientJob.Key.AssignedJob = clientJob.Value.Prefab; + clientJob.Key.AssignedJob = new Pair(clientJob.Value.Prefab, clientJob.Value.Variant); } } //count the clients who already have characters with an assigned job foreach (Client c in connectedClients) { - if (c.TeamID != teamID || unassigned.Contains(c)) continue; + if (c.TeamID != teamID || unassigned.Contains(c)) { continue; } if (c.Character?.Info?.Job != null && !c.Character.IsDead) { assignedClientCount[c.Character.Info.Job.Prefab]++; @@ -2961,8 +2985,8 @@ namespace Barotrauma.Networking //if any of the players has chosen a job that is Always Allowed, give them that job for (int i = unassigned.Count - 1; i >= 0; i--) { - if (unassigned[i].JobPreferences.Count == 0) continue; - if (!unassigned[i].JobPreferences[0].AllowAlways) continue; + if (unassigned[i].JobPreferences.Count == 0) { continue; } + if (!unassigned[i].JobPreferences[0].First.AllowAlways) { continue; } unassigned[i].AssignedJob = unassigned[i].JobPreferences[0]; unassigned.RemoveAt(i); } @@ -2975,32 +2999,75 @@ namespace Barotrauma.Networking foreach (JobPrefab jobPrefab in jobList) { - if (unassigned.Count == 0) break; - if (jobPrefab.MinNumber < 1 || assignedClientCount[jobPrefab] >= jobPrefab.MinNumber) continue; + if (unassigned.Count == 0) { break; } + if (jobPrefab.MinNumber < 1 || assignedClientCount[jobPrefab] >= jobPrefab.MinNumber) { continue; } //find the client that wants the job the most, or force it to random client if none of them want it Client assignedClient = FindClientWithJobPreference(unassigned, jobPrefab, true); - assignedClient.AssignedJob = jobPrefab; + assignedClient.AssignedJob = + assignedClient.JobPreferences.FirstOrDefault(jp => jp.First == jobPrefab) ?? + new Pair(jobPrefab, 0); + assignedClientCount[jobPrefab]++; unassigned.Remove(assignedClient); //the job still needs more crew members, set unassignedJobsFound to true to keep the while loop running - if (assignedClientCount[jobPrefab] < jobPrefab.MinNumber) unassignedJobsFound = true; + if (assignedClientCount[jobPrefab] < jobPrefab.MinNumber) { unassignedJobsFound = true; } } } + List availableSpawnPoints = WayPoint.WayPointList.FindAll(wp => + wp.SpawnType == SpawnType.Human && + wp.Submarine != null && wp.Submarine.TeamID == teamID); + List unassignedSpawnPoints = new List(availableSpawnPoints); + + /*bool canAssign = false; + do + { + canAssign = false; + foreach (WayPoint spawnPoint in unassignedSpawnPoints) + { + if (unassigned.Count == 0) { break; } + + JobPrefab job = spawnPoint.AssignedJob ?? JobPrefab.List.Values.GetRandom(); + if (assignedClientCount[job] >= job.MaxNumber) { continue; } + + Client assignedClient = FindClientWithJobPreference(unassigned, job, true); + if (assignedClient != null) + { + assignedClient.AssignedJob = job; + assignedClientCount[job]++; + unassigned.Remove(assignedClient); + canAssign = true; + } + } + } while (unassigned.Count > 0 && canAssign);*/ + //attempt to give the clients a job they have in their job preferences for (int i = unassigned.Count - 1; i >= 0; i--) { - foreach (JobPrefab preferredJob in unassigned[i].JobPreferences) + if (unassignedSpawnPoints.Count == 0) { break; } + foreach (Pair preferredJob in unassigned[i].JobPreferences) { - //the maximum number of players that can have this job hasn't been reached yet - // -> assign it to the client - if (assignedClientCount[preferredJob] < preferredJob.MaxNumber && unassigned[i].Karma >= preferredJob.MinKarma) + //can't assign this job if maximum number has reached or the clien't karma is too low + if (assignedClientCount[preferredJob.First] >= preferredJob.First.MaxNumber || unassigned[i].Karma < preferredJob.First.MinKarma) { + continue; + } + //give the client their preferred job if there's a spawnpoint available for that job + var matchingSpawnPoint = unassignedSpawnPoints.Find(s => s.AssignedJob == preferredJob.First); + //if the job is not available in any spawnpoint (custom job?), treat empty spawnpoints + //as a matching ones + if (matchingSpawnPoint == null && !availableSpawnPoints.Any(s => s.AssignedJob == preferredJob.First)) + { + matchingSpawnPoint = unassignedSpawnPoints.Find(s => s.AssignedJob == null); + } + if (matchingSpawnPoint != null) + { + unassignedSpawnPoints.Remove(matchingSpawnPoint); unassigned[i].AssignedJob = preferredJob; - assignedClientCount[preferredJob]++; + assignedClientCount[preferredJob.First]++; unassigned.RemoveAt(i); break; } @@ -3023,25 +3090,36 @@ namespace Barotrauma.Networking { jobIndex++; skips++; - if (jobIndex >= jobList.Count) jobIndex -= jobList.Count; - if (skips >= jobList.Count) break; + if (jobIndex >= jobList.Count) { jobIndex -= jobList.Count; } + if (skips >= jobList.Count) { break; } } - c.AssignedJob = jobList[jobIndex]; - assignedClientCount[c.AssignedJob]++; + c.AssignedJob = + c.JobPreferences.FirstOrDefault(jp => jp.First == jobList[jobIndex]) ?? + new Pair(jobList[jobIndex], 0); + assignedClientCount[c.AssignedJob.First]++; } - else //some jobs still left, choose one of them by random + //if one of the client's preferences is still available, give them that job + else if (c.JobPreferences.Any(jp => remainingJobs.Contains(jp.First))) { - c.AssignedJob = remainingJobs[Rand.Range(0, remainingJobs.Count)]; - assignedClientCount[c.AssignedJob]++; + foreach (Pair preferredJob in c.JobPreferences) + { + c.AssignedJob = preferredJob; + assignedClientCount[preferredJob.First]++; + break; + } + } + else //none of the client's preferred jobs available, choose a random job + { + c.AssignedJob = new Pair(remainingJobs[Rand.Range(0, remainingJobs.Count)], 0); + assignedClientCount[c.AssignedJob.First]++; } } } public void AssignBotJobs(List bots, Character.TeamType teamID) { - var jobList = JobPrefab.List.Values.ToList(); Dictionary assignedPlayerCount = new Dictionary(); - foreach (JobPrefab jp in jobList) + foreach (JobPrefab jp in JobPrefab.List.Values) { assignedPlayerCount.Add(jp, 0); } @@ -3061,25 +3139,39 @@ namespace Barotrauma.Networking } List unassignedBots = new List(bots); - foreach (CharacterInfo bot in bots) - { - foreach (JobPrefab jobPrefab in jobList) - { - if (jobPrefab.MinNumber < 1 || assignedPlayerCount[jobPrefab] >= jobPrefab.MinNumber) continue; - bot.Job = new Job(jobPrefab); - assignedPlayerCount[jobPrefab]++; - unassignedBots.Remove(bot); - break; - } - } - //find a suitable job for the rest of the players + List spawnPoints = WayPoint.WayPointList.FindAll(wp => + wp.SpawnType == SpawnType.Human && + wp.Submarine != null && wp.Submarine.TeamID == teamID) + .OrderBy(sp => Rand.Int(int.MaxValue)) + .OrderBy(sp => sp.AssignedJob == null ? 0 : 1) + .ToList(); + + bool canAssign = false; + do + { + canAssign = false; + foreach (WayPoint spawnPoint in spawnPoints) + { + if (unassignedBots.Count == 0) { break; } + + JobPrefab jobPrefab = spawnPoint.AssignedJob ?? JobPrefab.List.Values.GetRandom(); + if (assignedPlayerCount[jobPrefab] >= jobPrefab.MaxNumber) { continue; } + + unassignedBots[0].Job = new Job(jobPrefab); + assignedPlayerCount[jobPrefab]++; + unassignedBots.Remove(unassignedBots[0]); + canAssign = true; + } + } while (unassignedBots.Count > 0 && canAssign); + + //find a suitable job for the rest of the bots foreach (CharacterInfo c in unassignedBots) { //find all jobs that are still available - var remainingJobs = jobList.FindAll(jp => assignedPlayerCount[jp] < jp.MaxNumber); + var remainingJobs = JobPrefab.List.Values.Where(jp => assignedPlayerCount[jp] < jp.MaxNumber); //all jobs taken, give a random job - if (remainingJobs.Count == 0) + if (remainingJobs.Count() == 0) { DebugConsole.ThrowError("Failed to assign a suitable job for bot \"" + c.Name + "\" (all jobs already have the maximum numbers of players). Assigning a random job..."); c.Job = Job.Random(); @@ -3087,7 +3179,7 @@ namespace Barotrauma.Networking } else //some jobs still left, choose one of them by random { - c.Job = new Job(remainingJobs[Rand.Range(0, remainingJobs.Count)]); + c.Job = new Job(remainingJobs.GetRandom()); assignedPlayerCount[c.Job.Prefab]++; } } @@ -3100,7 +3192,7 @@ namespace Barotrauma.Networking foreach (Client c in clients) { if (c.Karma < job.MinKarma) continue; - int index = c.JobPreferences.IndexOf(job); + int index = c.JobPreferences.IndexOf(c.JobPreferences.Find(j => j.First == job)); if (index == -1) index = 1000; if (preferredClient == null || index < bestPreference) @@ -3119,6 +3211,17 @@ namespace Barotrauma.Networking return preferredClient; } + public void UpdateMissionState(int state) + { + foreach (var client in connectedClients) + { + IWriteMessage msg = new WriteOnlyMessage(); + msg.Write((byte)ServerPacketHeader.MISSION); + msg.Write((ushort)state); + serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); + } + } + public static void Log(string line, ServerLog.MessageType messageType) { if (GameMain.Server == null || !GameMain.Server.ServerSettings.SaveServerLogs) return; @@ -3152,6 +3255,9 @@ namespace Barotrauma.Networking started = false; serverSettings.BanList.Save(); + + if (GameMain.NetLobbyScreen.SelectedSub != null) { serverSettings.SelectedSubmarine = GameMain.NetLobbyScreen.SelectedSub.Name; } + if (GameMain.NetLobbyScreen.SelectedShuttle != null) { serverSettings.SelectedShuttle = GameMain.NetLobbyScreen.SelectedShuttle.Name; } serverSettings.SaveSettings(); if (registeredToMaster) diff --git a/Barotrauma/BarotraumaServer/Source/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs b/Barotrauma/BarotraumaServer/Source/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs index bf755a911..3d31b9f6b 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs @@ -342,7 +342,7 @@ namespace Barotrauma.Networking if (!Client.IsValidName(name, serverSettings)) { if (OwnerConnection != null || - !IPAddress.IsLoopback(pendingClient.Connection.RemoteEndPoint.Address.MapToIPv4()) && + !IPAddress.IsLoopback(pendingClient.Connection.RemoteEndPoint.Address.MapToIPv4NoThrow()) && ownerKey == null || ownKey == 0 && ownKey != ownerKey) { RemovePendingClient(pendingClient, DisconnectReason.InvalidName, "The name \"" + name + "\" is invalid"); @@ -362,36 +362,37 @@ namespace Barotrauma.Networking return; } - Int32 contentPackageCount = inc.ReadVariableInt32(); - List contentPackages = new List(); + int contentPackageCount = inc.ReadVariableInt32(); + List clientContentPackages = new List(); for (int i = 0; i < contentPackageCount; i++) { string packageName = inc.ReadString(); string packageHash = inc.ReadString(); - contentPackages.Add(new ClientContentPackage(packageName, packageHash)); + clientContentPackages.Add(new ClientContentPackage(packageName, packageHash)); } + //check if the client is missing any of our packages List missingPackages = new List(); - foreach (ContentPackage contentPackage in GameMain.SelectedPackages) + foreach (ContentPackage serverContentPackage in GameMain.SelectedPackages) { - if (!contentPackage.HasMultiplayerIncompatibleContent) continue; - bool packageFound = false; - for (int i = 0; i < contentPackageCount; i++) - { - if (contentPackages[i].Name == contentPackage.Name && contentPackages[i].Hash == contentPackage.MD5hash.Hash) - { - packageFound = true; - break; - } - } - if (!packageFound) missingPackages.Add(contentPackage); + if (!serverContentPackage.HasMultiplayerIncompatibleContent) continue; + bool packageFound = clientContentPackages.Any(cp => cp.Name == serverContentPackage.Name && cp.Hash == serverContentPackage.MD5hash.Hash); + if (!packageFound) { missingPackages.Add(serverContentPackage); } + } + + //check if the client is using packages we don't have + List redundantPackages = new List(); + foreach (ClientContentPackage clientContentPackage in clientContentPackages) + { + bool packageFound = GameMain.SelectedPackages.Any(cp => cp.Name == clientContentPackage.Name && cp.MD5hash.Hash == clientContentPackage.Hash); + if (!packageFound) { redundantPackages.Add(clientContentPackage); } } if (missingPackages.Count == 1) { RemovePendingClient(pendingClient, DisconnectReason.MissingContentPackage, $"DisconnectMessage.MissingContentPackage~[missingcontentpackage]={GetPackageStr(missingPackages[0])}"); - GameServer.Log(name + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (missing content package " + GetPackageStr(missingPackages[0]) + ")", ServerLog.MessageType.Error); + GameServer.Log(name + " (" + inc.SenderConnection.RemoteEndPoint.Address + ") couldn't join the server (missing content package " + GetPackageStr(missingPackages[0]) + ")", ServerLog.MessageType.Error); return; } else if (missingPackages.Count > 1) @@ -400,7 +401,23 @@ namespace Barotrauma.Networking missingPackages.ForEach(cp => packageStrs.Add(GetPackageStr(cp))); RemovePendingClient(pendingClient, DisconnectReason.MissingContentPackage, $"DisconnectMessage.MissingContentPackages~[missingcontentpackages]={string.Join(", ", packageStrs)}"); - GameServer.Log(name + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (missing content packages " + string.Join(", ", packageStrs) + ")", ServerLog.MessageType.Error); + GameServer.Log(name + " (" + inc.SenderConnection.RemoteEndPoint.Address + ") couldn't join the server (missing content packages " + string.Join(", ", packageStrs) + ")", ServerLog.MessageType.Error); + return; + } + if (redundantPackages.Count == 1) + { + RemovePendingClient(pendingClient, DisconnectReason.IncompatibleContentPackage, + $"DisconnectMessage.IncompatibleContentPackage~[incompatiblecontentpackage]={GetPackageStr(redundantPackages[0])}"); + GameServer.Log(name + " (" + inc.SenderConnection.RemoteEndPoint.Address + ") couldn't join the server (using an incompatible content package " + GetPackageStr(redundantPackages[0]) + ")", ServerLog.MessageType.Error); + return; + } + if (redundantPackages.Count > 1) + { + List packageStrs = new List(); + redundantPackages.ForEach(cp => packageStrs.Add(GetPackageStr(cp))); + RemovePendingClient(pendingClient, DisconnectReason.IncompatibleContentPackage, + $"DisconnectMessage.IncompatibleContentPackages~[incompatiblecontentpackages]={string.Join(", ", packageStrs)}"); + GameServer.Log(name + " (" + inc.SenderConnection.RemoteEndPoint.Address + ") couldn't join the server (using incompatible content packages " + string.Join(", ", packageStrs) + ")", ServerLog.MessageType.Error); return; } @@ -476,21 +493,6 @@ namespace Barotrauma.Networking } } - protected struct ClientContentPackage - { - public string Name; - public string Hash; - - public ClientContentPackage(string name, string hash) - { - Name = name; Hash = hash; - } - } - - private string GetPackageStr(ContentPackage contentPackage) - { - return "\"" + contentPackage.Name + "\" (hash " + contentPackage.MD5hash.ShortHash + ")"; - } private void UpdatePendingClient(PendingClient pendingClient, float deltaTime) { @@ -519,7 +521,7 @@ namespace Barotrauma.Networking pendingClients.Remove(pendingClient); if (OwnerConnection == null && - IPAddress.IsLoopback(pendingClient.Connection.RemoteEndPoint.Address.MapToIPv4()) && + IPAddress.IsLoopback(pendingClient.Connection.RemoteEndPoint.Address.MapToIPv4NoThrow()) && ownerKey != null && pendingClient.OwnerKey != 0 && pendingClient.OwnerKey == ownerKey) { ownerKey = null; diff --git a/Barotrauma/BarotraumaServer/Source/Networking/Primitives/Peers/Server/ServerPeer.cs b/Barotrauma/BarotraumaServer/Source/Networking/Primitives/Peers/Server/ServerPeer.cs index 62ac3fc0b..3977dd95b 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/Primitives/Peers/Server/ServerPeer.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/Primitives/Peers/Server/ServerPeer.cs @@ -1,12 +1,33 @@ using Facepunch.Steamworks; using System; using System.Collections.Generic; +using System.Linq; using System.Text; namespace Barotrauma.Networking { abstract class ServerPeer { + protected struct ClientContentPackage + { + public string Name; + public string Hash; + + public ClientContentPackage(string name, string hash) + { + Name = name; Hash = hash; + } + } + + protected string GetPackageStr(ContentPackage contentPackage) + { + return "\"" + contentPackage.Name + "\" (hash " + contentPackage.MD5hash.ShortHash + ")"; + } + protected string GetPackageStr(ClientContentPackage contentPackage) + { + return "\"" + contentPackage.Name + "\" (hash " + Md5Hash.GetShortHash(contentPackage.Hash) + ")"; + } + public delegate void MessageCallback(NetworkConnection connection, IReadMessage message); public delegate void DisconnectCallback(NetworkConnection connection, string reason); public delegate void InitializationCompleteCallback(NetworkConnection connection); @@ -28,6 +49,8 @@ namespace Barotrauma.Networking public abstract void Start(); public abstract void Close(string msg = null); public abstract void Update(float deltaTime); + + public abstract void Send(IWriteMessage msg, NetworkConnection conn, DeliveryMethod deliveryMethod); public abstract void Disconnect(NetworkConnection conn, string msg = null); } diff --git a/Barotrauma/BarotraumaServer/Source/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs b/Barotrauma/BarotraumaServer/Source/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs index 43842c8f8..694428be6 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs @@ -200,7 +200,7 @@ namespace Barotrauma.Networking return; } - if (IPAddress.IsLoopback(inc.SenderConnection.RemoteEndPoint.Address.MapToIPv4())) + if (IPAddress.IsLoopback(inc.SenderConnection.RemoteEndPoint.Address.MapToIPv4NoThrow())) { inc.SenderConnection.Approve(); netConnection = inc.SenderConnection; @@ -403,35 +403,36 @@ namespace Barotrauma.Networking } int contentPackageCount = (int)inc.ReadVariableUInt32(); - List contentPackages = new List(); + List clientContentPackages = new List(); for (int i = 0; i < contentPackageCount; i++) { string packageName = inc.ReadString(); string packageHash = inc.ReadString(); - contentPackages.Add(new ClientContentPackage(packageName, packageHash)); + clientContentPackages.Add(new ClientContentPackage(packageName, packageHash)); } + //check if the client is missing any of our packages List missingPackages = new List(); - foreach (ContentPackage contentPackage in GameMain.SelectedPackages) + foreach (ContentPackage serverContentPackage in GameMain.SelectedPackages) { - if (!contentPackage.HasMultiplayerIncompatibleContent) continue; - bool packageFound = false; - for (int i = 0; i < (int)contentPackageCount; i++) - { - if (contentPackages[i].Name == contentPackage.Name && contentPackages[i].Hash == contentPackage.MD5hash.Hash) - { - packageFound = true; - break; - } - } - if (!packageFound) missingPackages.Add(contentPackage); + if (!serverContentPackage.HasMultiplayerIncompatibleContent) continue; + bool packageFound = clientContentPackages.Any(cp => cp.Name == serverContentPackage.Name && cp.Hash == serverContentPackage.MD5hash.Hash); + if (!packageFound) { missingPackages.Add(serverContentPackage); } + } + + //check if the client is using packages we don't have + List redundantPackages = new List(); + foreach (ClientContentPackage clientContentPackage in clientContentPackages) + { + bool packageFound = GameMain.SelectedPackages.Any(cp => cp.Name == clientContentPackage.Name && cp.MD5hash.Hash == clientContentPackage.Hash); + if (!packageFound) { redundantPackages.Add(clientContentPackage); } } if (missingPackages.Count == 1) { RemovePendingClient(pendingClient, DisconnectReason.MissingContentPackage, $"DisconnectMessage.MissingContentPackage~[missingcontentpackage]={GetPackageStr(missingPackages[0])}"); - GameServer.Log(name + " (" + pendingClient.SteamID.ToString() + ") couldn't join the server (missing content package " + GetPackageStr(missingPackages[0]) + ")", ServerLog.MessageType.Error); + GameServer.Log(name + " (" + pendingClient.SteamID + ") couldn't join the server (missing content package " + GetPackageStr(missingPackages[0]) + ")", ServerLog.MessageType.Error); return; } else if (missingPackages.Count > 1) @@ -440,7 +441,23 @@ namespace Barotrauma.Networking missingPackages.ForEach(cp => packageStrs.Add(GetPackageStr(cp))); RemovePendingClient(pendingClient, DisconnectReason.MissingContentPackage, $"DisconnectMessage.MissingContentPackages~[missingcontentpackages]={string.Join(", ", packageStrs)}"); - GameServer.Log(name + " (" + pendingClient.SteamID.ToString() + ") couldn't join the server (missing content packages " + string.Join(", ", packageStrs) + ")", ServerLog.MessageType.Error); + GameServer.Log(name + " (" + pendingClient.SteamID + ") couldn't join the server (missing content packages " + string.Join(", ", packageStrs) + ")", ServerLog.MessageType.Error); + return; + } + if (redundantPackages.Count == 1) + { + RemovePendingClient(pendingClient, DisconnectReason.IncompatibleContentPackage, + $"DisconnectMessage.IncompatibleContentPackage~[incompatiblecontentpackage]={GetPackageStr(redundantPackages[0])}"); + GameServer.Log(name + " (" + pendingClient.SteamID + ") couldn't join the server (using an incompatible content package " + GetPackageStr(redundantPackages[0]) + ")", ServerLog.MessageType.Error); + return; + } + if (redundantPackages.Count > 1) + { + List packageStrs = new List(); + redundantPackages.ForEach(cp => packageStrs.Add(GetPackageStr(cp))); + RemovePendingClient(pendingClient, DisconnectReason.IncompatibleContentPackage, + $"DisconnectMessage.IncompatibleContentPackages~[incompatiblecontentpackages]={string.Join(", ", packageStrs)}"); + GameServer.Log(name + " (" + pendingClient.SteamID + ") couldn't join the server (using incompatible content packages " + string.Join(", ", packageStrs) + ")", ServerLog.MessageType.Error); return; } @@ -482,21 +499,6 @@ namespace Barotrauma.Networking } } - protected struct ClientContentPackage - { - public string Name; - public string Hash; - - public ClientContentPackage(string name, string hash) - { - Name = name; Hash = hash; - } - } - - private string GetPackageStr(ContentPackage contentPackage) - { - return "\"" + contentPackage.Name + "\" (hash " + contentPackage.MD5hash.ShortHash + ")"; - } private void UpdatePendingClient(PendingClient pendingClient) { diff --git a/Barotrauma/BarotraumaServer/Source/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/Source/Networking/RespawnManager.cs index eab5e7431..0c53224d8 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/RespawnManager.cs @@ -235,7 +235,7 @@ namespace Barotrauma.Networking GameMain.Server.AssignJobs(clients); foreach (Client c in clients) { - c.CharacterInfo.Job = new Job(c.AssignedJob); + c.CharacterInfo.Job = new Job(c.AssignedJob.First, c.AssignedJob.Second); } //the spawnpoints where the characters will spawn diff --git a/Barotrauma/BarotraumaServer/Source/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/Source/Networking/ServerSettings.cs index 9a218b8a4..a1e7b414b 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/ServerSettings.cs @@ -128,10 +128,9 @@ namespace Barotrauma.Networking if (flags.HasFlag(NetFlags.Misc)) { - int missionType = GameMain.NetLobbyScreen.MissionTypeIndex + incMsg.ReadByte() - 1; - while (missionType < 0) missionType += Enum.GetValues(typeof(MissionType)).Length; - while (missionType >= Enum.GetValues(typeof(MissionType)).Length) missionType -= Enum.GetValues(typeof(MissionType)).Length; - GameMain.NetLobbyScreen.MissionTypeIndex = missionType; + int orBits = incMsg.ReadRangedInteger(0, (int)Barotrauma.MissionType.All) & (int)Barotrauma.MissionType.All; + int andBits = incMsg.ReadRangedInteger(0, (int)Barotrauma.MissionType.All) & (int)Barotrauma.MissionType.All; + GameMain.NetLobbyScreen.MissionType = (Barotrauma.MissionType)(((int)GameMain.NetLobbyScreen.MissionType | orBits) & andBits); int traitorSetting = (int)TraitorsEnabled + incMsg.ReadByte() - 1; if (traitorSetting < 0) traitorSetting = 2; @@ -310,6 +309,9 @@ namespace Barotrauma.Networking ServerMessageText = doc.Root.GetAttributeString("ServerMessage", ""); GameMain.NetLobbyScreen.SelectedModeIdentifier = GameModeIdentifier; + //handle Random as the mission type, which is no longer a valid setting + //MissionType.All offers equivalent functionality + if (MissionType == "Random") { MissionType = "All"; } GameMain.NetLobbyScreen.MissionTypeName = MissionType; GameMain.NetLobbyScreen.SetBotSpawnMode(BotSpawnMode); diff --git a/Barotrauma/BarotraumaServer/Source/Networking/SteamManager.cs b/Barotrauma/BarotraumaServer/Source/Networking/SteamManager.cs index c15bef58d..982965209 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/SteamManager.cs @@ -62,6 +62,8 @@ namespace Barotrauma.Steam Instance.server.SetKey("modeselectionmode", server.ServerSettings.ModeSelectionMode.ToString()); Instance.server.SetKey("subselectionmode", server.ServerSettings.SubSelectionMode.ToString()); Instance.server.SetKey("voicechatenabled", server.ServerSettings.VoiceChatEnabled.ToString()); + Instance.server.SetKey("karmaenabled", server.ServerSettings.KarmaEnabled.ToString()); + Instance.server.SetKey("friendlyfireenabled", server.ServerSettings.AllowFriendlyFire.ToString()); Instance.server.SetKey("allowspectating", server.ServerSettings.AllowSpectating.ToString()); Instance.server.SetKey("allowrespawn", server.ServerSettings.AllowRespawn.ToString()); Instance.server.SetKey("traitors", server.ServerSettings.TraitorsEnabled.ToString()); diff --git a/Barotrauma/BarotraumaServer/Source/Networking/WhiteList.cs b/Barotrauma/BarotraumaServer/Source/Networking/WhiteList.cs index 9ab32db6d..c50de1630 100644 --- a/Barotrauma/BarotraumaServer/Source/Networking/WhiteList.cs +++ b/Barotrauma/BarotraumaServer/Source/Networking/WhiteList.cs @@ -101,7 +101,7 @@ namespace Barotrauma.Networking if (wlp == null) return false; if (!string.IsNullOrWhiteSpace(wlp.IP)) { - if (address.IsIPv4MappedToIPv6 && wlp.IP == address.MapToIPv4().ToString()) + if (address.IsIPv4MappedToIPv6 && wlp.IP == address.MapToIPv4NoThrow().ToString()) { return true; } diff --git a/Barotrauma/BarotraumaServer/Source/Program.cs b/Barotrauma/BarotraumaServer/Source/Program.cs index 9e7989cd3..fd725f717 100644 --- a/Barotrauma/BarotraumaServer/Source/Program.cs +++ b/Barotrauma/BarotraumaServer/Source/Program.cs @@ -25,7 +25,7 @@ namespace Barotrauma { GameMain game = null; -#if !DEBUG +#if !DEBUG || TRUE try { #endif @@ -49,7 +49,7 @@ namespace Barotrauma DebugConsole.InputThread?.Abort(); DebugConsole.InputThread?.Join(); if (GameSettings.SendUserStatistics) GameAnalytics.OnQuit(); SteamManager.ShutDown(); -#if !DEBUG +#if !DEBUG || TRUE } catch (Exception e) { diff --git a/Barotrauma/BarotraumaServer/Source/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaServer/Source/Screens/NetLobbyScreen.cs index 2c2d78668..5eb445456 100644 --- a/Barotrauma/BarotraumaServer/Source/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaServer/Source/Screens/NetLobbyScreen.cs @@ -74,26 +74,28 @@ namespace Barotrauma get { return GameModes[SelectedModeIndex]; } } - private int missionTypeIndex; - public int MissionTypeIndex + private MissionType missionType; + public MissionType MissionType { - get { return missionTypeIndex; } + get { return missionType; } set { lastUpdateID++; - missionTypeIndex = MathHelper.Clamp(value, 0, Enum.GetValues(typeof(MissionType)).Length - 1); + missionType = value; + if (GameMain.NetworkMember?.ServerSettings != null) + { + GameMain.NetworkMember.ServerSettings.MissionType = missionType.ToString(); + } } } public string MissionTypeName { - get { return ((MissionType)missionTypeIndex).ToString(); } + get { return missionType.ToString(); } set { - if (Enum.TryParse(value, out MissionType missionType)) - { - missionTypeIndex = (int)missionType; - } + Enum.TryParse(value, out MissionType type); + MissionType = type; } } diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalDestroyItemsWithTag.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalDestroyItemsWithTag.cs index 23e129954..551158b07 100644 --- a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalDestroyItemsWithTag.cs +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalDestroyItemsWithTag.cs @@ -60,6 +60,13 @@ namespace Barotrauma ++result; } } + + // Quick fix + if (tagPrefabName == null && matchIdentifier) + { + tagPrefabName = TextManager.FormatServerMessage($"entityname.{tag}"); + } + return result; } diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalEntityTransformation.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalEntityTransformation.cs new file mode 100644 index 000000000..2472d685f --- /dev/null +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalEntityTransformation.cs @@ -0,0 +1,162 @@ +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + partial class Traitor + { + public sealed class GoalEntityTransformation : Goal + { + public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[catalystitem]" }); + public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { catalystItemName }); + + private bool isCompleted; + public override bool IsCompleted => isCompleted; + + private string catalystItemIdentifier, catalystItemName; + + private Vector2 activeEntitySavedPosition; + private Entity activeEntity; + private int activeEntityIndex; + private const float gracePeriod = 1f; + private const float graceDistance = 200f; + private float graceTimer; + private double transformationTime; + + private enum EntityTypes { Character, Item } + + private string[] entities; + private EntityTypes[] entityTypes; + + public override void Update(float deltaTime) + { + base.Update(deltaTime); + isCompleted = HasTransformed(deltaTime); + } + + public override bool CanBeCompleted(ICollection traitors) + { + return graceTimer <= gracePeriod; + } + + private bool HasTransformed(float deltaTime) + { + if (activeEntity != null && !activeEntity.Removed) + { + activeEntitySavedPosition = activeEntity.WorldPosition; + } + else + { + if (transformationTime == 0) + { + graceTimer = 0.0f; + activeEntityIndex++; + transformationTime = Timing.TotalTime; + } + graceTimer += deltaTime; + + switch (entityTypes[activeEntityIndex]) + { + case EntityTypes.Character: + foreach (Character character in Character.CharacterList) + { + if (character.Submarine == null || Traitors.All(t => character.Submarine.TeamID != t.Character.TeamID) || character.SpawnTime + gracePeriod < transformationTime) + { + continue; + } + if (character.SpeciesName.ToLowerInvariant() == entities[activeEntityIndex] && Vector2.Distance(activeEntitySavedPosition, character.WorldPosition) < graceDistance) + { + activeEntity = character; + transformationTime = 0.0; + return activeEntityIndex == entities.Length - 1; + } + } + break; + case EntityTypes.Item: + foreach (Item item in Item.ItemList) + { + if (item.Submarine == null || Traitors.All(t => item.Submarine.TeamID != t.Character.TeamID) || item.SpawnTime + gracePeriod < transformationTime) + { + continue; + } + if (item.prefab.Identifier == entities[activeEntityIndex] && Vector2.Distance(activeEntitySavedPosition, item.WorldPosition) < graceDistance) + { + activeEntity = item; + transformationTime = 0.0; + return activeEntityIndex == entities.Length - 1; + } + } + break; + } + } + + return false; + } + + public override bool Start(Traitor traitor) + { + if (!base.Start(traitor)) + { + return false; + } + + catalystItemName = TextManager.FormatServerMessage($"entityname.{catalystItemIdentifier}"); + + activeEntity = null; + activeEntityIndex = 0; + + switch (entityTypes[activeEntityIndex]) + { + case EntityTypes.Character: + foreach (Character character in Character.CharacterList) + { + if (character.Submarine == null || Traitors.All(t => character.Submarine.TeamID != t.Character.TeamID)) + { + continue; + } + if (character.SpeciesName.ToLowerInvariant() == entities[activeEntityIndex].ToLowerInvariant()) + { + activeEntity = character; + break; + } + } + break; + case EntityTypes.Item: + foreach (Item item in Item.ItemList) + { + if (item.Submarine == null || Traitors.All(t => item.Submarine.TeamID != t.Character.TeamID)) + { + continue; + } + if (item.prefab.Identifier.ToLowerInvariant() == entities[0].ToLowerInvariant()) + { + activeEntity = item; + break; + } + } + break; + } + + graceTimer = 0.0f; + return activeEntity != null; + } + + public GoalEntityTransformation(string[] entities, string[] entityTypes, string catalystItemIdentifier) : base() + { + this.entities = entities; + + this.entityTypes = new EntityTypes[entityTypes.Length]; + + for (int i = 0; i < this.entityTypes.Length; i++) + { + this.entityTypes[i] = (EntityTypes)Enum.Parse(typeof(EntityTypes), entityTypes[i], true); + } + + this.catalystItemIdentifier = catalystItemIdentifier; + } + } + } +} diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalFindItem.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalFindItem.cs index 70970e954..e1da8bcac 100644 --- a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalFindItem.cs +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalFindItem.cs @@ -1,5 +1,6 @@ using Barotrauma.Items.Components; using Barotrauma.Networking; +using System; using System.Collections.Generic; using System.Linq; @@ -9,6 +10,7 @@ namespace Barotrauma { public class GoalFindItem : HumanoidGoal { + private readonly TraitorMission.CharacterFilter filter; private readonly string identifier; private readonly bool preferNew; private readonly bool allowNew; @@ -16,12 +18,17 @@ namespace Barotrauma private readonly HashSet allowedContainerIdentifiers = new HashSet(); private ItemPrefab targetPrefab; + private ItemPrefab containedPrefab; private Item targetContainer; private Item target; private HashSet existingItems = new HashSet(); private string targetNameText; private string targetContainerNameText; private string targetHullNameText; + private float percentage; + private int spawnAmount = 1; + + private const string itemContainerId = "toolbox"; public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[identifier]", "[target]", "[targethullname]" }); public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { targetNameText ?? "", targetContainerNameText ?? "", targetHullNameText ?? "" }); @@ -85,7 +92,7 @@ namespace Barotrauma } if (suitableItems.Count == 0) { return null; } - return suitableItems[TraitorMission.Random(suitableItems.Count)]; + return suitableItems[TraitorManager.RandomInt(suitableItems.Count)]; } protected Item FindTargetContainer(ICollection traitors, ItemPrefab targetPrefabCandidate) @@ -124,12 +131,40 @@ namespace Barotrauma { return true; } - targetPrefab = FindItemPrefab(identifier); - if (targetPrefab == null) + + string targetPrefabTextId; + + if (percentage > 0f) { - return false; + spawnAmount = (int)Math.Floor(Character.CharacterList.FindAll(c => c.TeamID == traitor.Character.TeamID && c != traitor.Character && !c.IsDead && (filter == null || filter(c))).Count * percentage); } - var targetPrefabTextId = targetPrefab.GetItemNameTextId(); + + if (spawnAmount > 1 && allowNew) + { + containedPrefab = FindItemPrefab(identifier); + targetPrefab = FindItemPrefab(itemContainerId); + + if (containedPrefab == null || targetPrefab == null) + { + return false; + } + + targetPrefabTextId = containedPrefab.GetItemNameTextId(); + } + else + { + spawnAmount = 1; + containedPrefab = null; + targetPrefab = FindItemPrefab(identifier); + + if (targetPrefab == null) + { + return false; + } + + targetPrefabTextId = targetPrefab.GetItemNameTextId(); + } + targetNameText = targetPrefabTextId != null ? TextManager.FormatServerMessage(targetPrefabTextId) : targetPrefab.Name; targetContainer = FindTargetContainer(Traitors, targetPrefab); if (targetContainer == null) @@ -170,20 +205,29 @@ namespace Barotrauma base.Update(deltaTime); if (target == null) { - target = targetContainer.OwnInventory.Items.FirstOrDefault(item => item != null && item.Prefab.Identifier == identifier && !existingItems.Contains(item)); + target = targetContainer.OwnInventory.Items.FirstOrDefault(item => item != null && item.Prefab.Identifier == (containedPrefab != null ? itemContainerId : identifier) && !existingItems.Contains(item)); if (target != null) { + if (containedPrefab != null) + { + for (int i = 0; i < spawnAmount; i++) + { + Entity.Spawner.AddToSpawnQueue(containedPrefab, target.OwnInventory); + } + } existingItems.Clear(); } } } - public GoalFindItem(string identifier, bool preferNew, bool allowNew, bool allowExisting, params string[] allowedContainerIdentifiers) + public GoalFindItem(TraitorMission.CharacterFilter filter, string identifier, bool preferNew, bool allowNew, bool allowExisting, float percentage, params string[] allowedContainerIdentifiers) { + this.filter = filter; this.identifier = identifier; this.preferNew = preferNew; this.allowNew = allowNew; this.allowExisting = allowExisting; + this.percentage = percentage / 100f; this.allowedContainerIdentifiers.UnionWith(allowedContainerIdentifiers); } } diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalInjectTarget.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalInjectTarget.cs new file mode 100644 index 000000000..b33b65336 --- /dev/null +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalInjectTarget.cs @@ -0,0 +1,78 @@ +using Barotrauma.Networking; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + partial class Traitor + { + public sealed class GoalInjectTarget : Goal + { + public TraitorMission.CharacterFilter Filter { get; private set; } + public List Targets { get; private set; } + + public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[targetname]", "[poison]" }); + public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { traitor.Mission.GetTargetNames(Targets) ?? "(unknown)", poisonName }); + + private bool isCompleted = false; + public override bool IsCompleted => isCompleted; + + public override bool IsEnemy(Character character) => base.IsEnemy(character) || (!isCompleted && Targets.Contains(character)); + + private string poisonId; + private string afflictionId; + private string poisonName; + private int targetCount; + private float targetPercentage; + private bool[] targetWasInfected; + + public override void Update(float deltaTime) + { + base.Update(deltaTime); + isCompleted = WereAllTargetsInfected(); + } + + private bool WereAllTargetsInfected() + { + for (int i = 0; i < targetWasInfected.Length; i++) + { + if (targetWasInfected[i]) continue; + targetWasInfected[i] = Targets[i].CharacterHealth.GetAffliction(afflictionId) != null; + } + + return targetWasInfected.All(t => t == true); + } + + public override bool Start(Traitor traitor) + { + if (!base.Start(traitor)) + { + return false; + } + poisonName = TextManager.FormatServerMessage(poisonId) ?? poisonId; + + Targets = traitor.Mission.FindKillTarget(traitor.Character, Filter, targetCount, targetPercentage); + targetWasInfected = new bool[Targets.Count]; + return Targets != null && !Targets.All(t => t.IsDead); + } + + public GoalInjectTarget(TraitorMission.CharacterFilter filter, string poisonId, string afflictionId, int targetCount, float targetPercentage) : base() + { + Filter = filter; + this.poisonId = poisonId; + this.afflictionId = afflictionId; + this.targetCount = targetCount; + this.targetPercentage = targetPercentage / 100f; + + if (this.targetPercentage < 1.0f) + { + InfoTextId = "traitorgoalpoisoninfo"; + } + else + { + InfoTextId = "traitorgoalpoisoneveryoneinfo"; + } + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalKeepTransformedAlive.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalKeepTransformedAlive.cs new file mode 100644 index 000000000..6e40f2681 --- /dev/null +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalKeepTransformedAlive.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + partial class Traitor + { + public sealed class GoalKeepTransformedAlive : Goal + { + public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[speciesname]" }); + public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { targetCharacterName }); + + public override bool IsCompleted => isCompleted; + private bool isCompleted; + + private const float gracePeriod = 1f; + private string speciesId; + private string targetCharacterName; + private Character targetCharacter; + private float timer; + + public override bool CanBeCompleted(ICollection traitors) + { + return timer < gracePeriod || targetCharacter != null && !targetCharacter.IsDead; + } + + public override void Update(float deltaTime) + { + base.Update(deltaTime); + + if (timer <= gracePeriod) + { + timer += deltaTime; + } + + isCompleted = targetCharacter != null && !targetCharacter.IsDead && timer >= gracePeriod; + } + + public override bool Start(Traitor traitor) + { + if (!base.Start(traitor)) + { + return false; + } + + var startTime = Timing.TotalTime; + + foreach (Character character in Character.CharacterList) + { + if (character.Submarine == null || Traitors.All(t => character.Submarine.TeamID != t.Character.TeamID) || character.SpawnTime + gracePeriod < startTime) + { + continue; + } + if (character.SpeciesName.ToLowerInvariant() == speciesId) + { + targetCharacter = character; + break; + } + } + + targetCharacterName = TextManager.FormatServerMessage($"character.{speciesId}").ToLowerInvariant(); + + return targetCharacter != null; + } + + public GoalKeepTransformedAlive(string speciesId) : base() + { + this.speciesId = speciesId.ToLowerInvariant(); + } + } + } +} diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalKillTarget.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalKillTarget.cs index e8fad3b79..ca486e419 100644 --- a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalKillTarget.cs +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalKillTarget.cs @@ -9,20 +9,109 @@ namespace Barotrauma public sealed class GoalKillTarget : Goal { public TraitorMission.CharacterFilter Filter { get; private set; } - public Character Target { get; private set; } + public List Targets { get; private set; } - public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[targetname]" }); - public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { Target?.Name ?? "(unknown)" }); + public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[targetname]", "[causeofdeath]", "[targethullname]" }); + public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] + { traitor.Mission.GetTargetNames(Targets) ?? "(unknown)", GetCauseOfDeath(), targetHull != null ? TextManager.Get($"roomname.{targetHull}") : string.Empty }); private bool isCompleted = false; public override bool IsCompleted => isCompleted; - public override bool IsEnemy(Character character) => base.IsEnemy(character) || (!isCompleted && character == Target); + public override bool IsEnemy(Character character) => base.IsEnemy(character) || (!isCompleted && Targets.Contains(character)); + + private CauseOfDeathType requiredCauseOfDeath; + private string afflictionId; + private string targetHull; + private int targetCount; + private float targetPercentage; public override void Update(float deltaTime) { base.Update(deltaTime); - isCompleted = Target?.IsDead ?? false; + isCompleted = DoesDeathMatchCriteria(); + } + + private bool DoesDeathMatchCriteria() + { + if (Targets == null || Targets.Any(t => !t.IsDead)) return false; + + bool typeMatch = false; + + for (int i = 0; i < Targets.Count; i++) + { + // No specified cause of death required or missing cause of death + if (requiredCauseOfDeath == CauseOfDeathType.Unknown || Targets[i].CauseOfDeath == null) + { + typeMatch = true; + } + else + { + switch (Targets[i].CauseOfDeath.Type) + { + // If a cause of death is labeled as unknown, side with the traitor and accept this regardless of the required type + case CauseOfDeathType.Unknown: + typeMatch = true; + break; + case CauseOfDeathType.Pressure: + case CauseOfDeathType.Suffocation: + case CauseOfDeathType.Drowning: + typeMatch = requiredCauseOfDeath == Targets[i].CauseOfDeath.Type; + break; + case CauseOfDeathType.Affliction: + typeMatch = Targets[i].CauseOfDeath.Type == requiredCauseOfDeath && Targets[i].CauseOfDeath.Affliction.Identifier == afflictionId; + break; + case CauseOfDeathType.Disconnected: + typeMatch = false; + break; + } + } + + if (targetHull != null) + { + if (Targets[i].CurrentHull != null) + { + if (typeMatch && Targets[i].CurrentHull.RoomName == targetHull || Targets[i].CurrentHull.RoomName.Contains(targetHull)) + { + continue; + } + else + { + return false; + } + } + else + { + // Outside the submarine, not supported for now + return false; + } + } + else + { + if (typeMatch) + { + continue; + } + else + { + return false; + } + } + } + + return true; + } + + private string GetCauseOfDeath() + { + if (requiredCauseOfDeath != CauseOfDeathType.Affliction || afflictionId == string.Empty) + { + return requiredCauseOfDeath.ToString().ToLower(); + } + else + { + return TextManager.Get($"afflictionname.{afflictionId}").ToLower(); + } } public override bool Start(Traitor traitor) @@ -31,14 +120,43 @@ namespace Barotrauma { return false; } - Target = traitor.Mission.FindKillTarget(traitor.Character, Filter); - return Target != null && !Target.IsDead; + + Targets = traitor.Mission.FindKillTarget(traitor.Character, Filter, targetCount, targetPercentage); + return Targets != null && !Targets.All(t => t.IsDead); } - public GoalKillTarget(TraitorMission.CharacterFilter filter) : base() + public GoalKillTarget(TraitorMission.CharacterFilter filter, CauseOfDeathType requiredCauseOfDeath, string afflictionId, string targetHull, int targetCount, float targetPercentage) : base() { - InfoTextId = "TraitorGoalKillTargetInfo"; Filter = filter; + this.requiredCauseOfDeath = requiredCauseOfDeath; + this.afflictionId = afflictionId; + this.targetHull = targetHull; + this.targetCount = targetCount; + this.targetPercentage = targetPercentage / 100f; + + if (this.targetPercentage < 1f) + { + if (this.requiredCauseOfDeath == CauseOfDeathType.Unknown && targetHull == null) + { + InfoTextId = "traitorgoalkilltargetinfo"; + } + else if (this.requiredCauseOfDeath != CauseOfDeathType.Unknown && targetHull == null) + { + InfoTextId = "traitorgoalkilltargetinfowithcause"; + } + else if (this.requiredCauseOfDeath == CauseOfDeathType.Unknown && targetHull != null) + { + InfoTextId = "traitorgoalkilltargetinfowithhull"; + } + else if (this.requiredCauseOfDeath != CauseOfDeathType.Unknown && targetHull != null) + { + InfoTextId = "traitorgoalkilltargetinfowithcauseandhull"; + } + } + else + { + InfoTextId = "traitorgoalkilleveryoneinfo"; + } } } } diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalReachDistanceFromSub.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalReachDistanceFromSub.cs index 32c93a7f3..8db03ab3c 100644 --- a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalReachDistanceFromSub.cs +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalReachDistanceFromSub.cs @@ -12,9 +12,10 @@ namespace Barotrauma { private readonly float requiredDistance; private readonly float requiredDistanceSqr; + private float requiredDistanceInMeters; public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[distance]" }); - public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { $"{requiredDistance:0.00}" }); + public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { $"{requiredDistanceInMeters:0.00}" }); public override bool IsCompleted { @@ -22,12 +23,21 @@ namespace Barotrauma { return Traitors.Any(traitor => { - if (traitor.Character?.Submarine == null) + Submarine ownSub = null; + + for (int i = 0; i < Submarine.MainSubs.Length; i++) { - return false; + if (Submarine.MainSubs[i] != null && Submarine.MainSubs[i].TeamID == traitor.Character.TeamID) + { + ownSub = Submarine.MainSubs[i]; + break; + } } + + if (ownSub == null) return false; + var characterPosition = traitor.Character.WorldPosition; - var submarinePosition = traitor.Character.Submarine.WorldPosition; + var submarinePosition = ownSub.WorldPosition; var distance = Vector2.DistanceSquared(characterPosition, submarinePosition); return distance >= requiredDistanceSqr; }); @@ -37,8 +47,9 @@ namespace Barotrauma public GoalReachDistanceFromSub(float requiredDistance) : base() { InfoTextId = "TraitorGoalReachDistanceFromSub"; - this.requiredDistance = requiredDistance; - requiredDistanceSqr = requiredDistance * requiredDistance; + requiredDistanceInMeters = requiredDistance; + this.requiredDistance = requiredDistance / Physics.DisplayToRealWorldRatio; + requiredDistanceSqr = this.requiredDistance * this.requiredDistance; } } } diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalUnwiring.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalUnwiring.cs new file mode 100644 index 000000000..bcc4717aa --- /dev/null +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Goals/GoalUnwiring.cs @@ -0,0 +1,96 @@ +using Barotrauma.Items.Components; +using Barotrauma.Networking; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma +{ + partial class Traitor + { + public sealed class GoalUnwiring : HumanoidGoal + { + private readonly string tag; + + public override IEnumerable InfoTextKeys => base.InfoTextKeys.Concat(new string[] { "[targetname]", "[connectionname]" }); + public override IEnumerable InfoTextValues(Traitor traitor) => base.InfoTextValues(traitor).Concat(new string[] { targetItemPrefabName ?? "", targetConnectionDisplayName ?? targetConnectionName }); + + private bool isCompleted = false; + public override bool IsCompleted => isCompleted; + + private readonly List targetConnectionPanels = new List(); + private string targetItemPrefabName; + private string targetConnectionName; + private string targetConnectionDisplayName; + + public override bool Start(Traitor traitor) + { + if (!base.Start(traitor)) + { + return false; + } + foreach (var item in Item.ItemList) + { + if (item.Submarine == null || Traitors.All(t => item.Submarine.TeamID != t.Character.TeamID)) + { + continue; + } + if (item.Prefab?.Identifier == tag || item.HasTag(tag)) + { + var connectionPanel = item.GetComponent(); + if (connectionPanel != null) + { + targetConnectionPanels.Add(connectionPanel); + } + } + } + if (targetConnectionPanels.Count > 0) + { + var textId = targetConnectionPanels[0].Item.Prefab.GetItemNameTextId(); + targetItemPrefabName = TextManager.FormatServerMessage(textId) ?? targetConnectionPanels[0].Item.Prefab.Name; + } + + return targetConnectionPanels.Count > 0; + } + + public override void Update(float deltaTime) + { + base.Update(deltaTime); + isCompleted = AreTargetsUnwired(); + } + + private bool AreTargetsUnwired() + { + for (int i = 0; i < targetConnectionPanels.Count; i++) + { + for (int j = 0; j < targetConnectionPanels[i].Connections.Count; j++) + { + if (targetConnectionPanels[i].Connections[j] == null || targetConnectionPanels[i].Connections[j].Wires == null) continue; + if (targetConnectionName != string.Empty) + { + if (targetConnectionPanels[i].Connections[j].Name != targetConnectionName) continue; + } + if (!targetConnectionPanels[i].Connections[j].Wires.All(w => w == null)) return false; + } + } + + return true; + } + + public GoalUnwiring(string tag, string targetConnectionName, string targetConnectionDisplayTag) : base() + { + this.tag = tag; + this.targetConnectionName = targetConnectionName; + + if (targetConnectionDisplayTag != string.Empty) + { + targetConnectionDisplayName = TextManager.FormatServerMessage(targetConnectionDisplayTag); + InfoTextId = "TraitorGoalUnwireInfo"; + } + else + { + InfoTextId = "TraitorGoalUnwireAllInfo"; + } + } + } + } +} diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/Objective.cs b/Barotrauma/BarotraumaServer/Source/Traitors/Objective.cs index 48c4819fe..b8eaf65dc 100644 --- a/Barotrauma/BarotraumaServer/Source/Traitors/Objective.cs +++ b/Barotrauma/BarotraumaServer/Source/Traitors/Objective.cs @@ -101,7 +101,7 @@ namespace Barotrauma { for (var i = allGoalsCount; i > 1;) { - int j = TraitorMission.Random(i--); + int j = TraitorManager.RandomInt(i--); var temp = indices[j]; indices[j] = indices[i]; indices[i] = temp; @@ -125,10 +125,12 @@ namespace Barotrauma completedGoals.Add(goal); } } - if (pendingGoals.Count <= 0) + + if (pendingGoals.Count <= 0 && completedGoals.Count < allGoals.Count) { return false; } + IsStarted = true; traitor.SendChatMessageBox(StartMessageText, traitor.Mission?.Identifier); diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/TraitorManager.cs b/Barotrauma/BarotraumaServer/Source/Traitors/TraitorManager.cs index 2f71ffe1c..c973356bf 100644 --- a/Barotrauma/BarotraumaServer/Source/Traitors/TraitorManager.cs +++ b/Barotrauma/BarotraumaServer/Source/Traitors/TraitorManager.cs @@ -11,6 +11,14 @@ namespace Barotrauma { partial class TraitorManager { + public static readonly Random Random = new Random((int)DateTime.UtcNow.Ticks); + + // All traitor related functionality should use the following interface for generating random values + public static int RandomInt(int n) => Random.Next(n); + + // All traitor related functionality should use the following interface for generating random values + public static double RandomDouble() => Random.NextDouble(); + public readonly Dictionary Missions = new Dictionary(); public string GetCodeWords(Character.TeamType team) => Missions.TryGetValue(team, out var mission) ? mission.CodeWords : ""; @@ -21,33 +29,12 @@ namespace Barotrauma private float startCountdown = 0.0f; private GameServer server; - private readonly Dictionary traitorCountsBySteamId = new Dictionary(); - private readonly Dictionary traitorCountsByEndPoint = new Dictionary(); - public bool ShouldEndRound { get; set; } - public int GetTraitorCount(Tuple steamIdAndEndPoint) - { - if (steamIdAndEndPoint.Item1 > 0 && traitorCountsBySteamId.TryGetValue(steamIdAndEndPoint.Item1, out var steamIdResult)) - { - return steamIdResult; - } - return traitorCountsByEndPoint.TryGetValue(steamIdAndEndPoint.Item2, out var endPointResult) ? endPointResult : 0; - } - - public void SetTraitorCount(Tuple steamIdAndEndPoint, int count) - { - if (steamIdAndEndPoint.Item1 > 0) - { - traitorCountsBySteamId[steamIdAndEndPoint.Item1] = count; - } - traitorCountsByEndPoint[steamIdAndEndPoint.Item2] = count; - } - public bool IsTraitor(Character character) { if (Traitors == null) @@ -80,11 +67,13 @@ namespace Barotrauma ShouldEndRound = false; - Traitor.TraitorMission.InitializeRandom(); this.server = server; - startCountdown = MathHelper.Lerp(server.ServerSettings.TraitorsMinStartDelay, server.ServerSettings.TraitorsMaxStartDelay, (float)Traitor.TraitorMission.RandomDouble()); - traitorCountsBySteamId.Clear(); - traitorCountsByEndPoint.Clear(); + startCountdown = MathHelper.Lerp(server.ServerSettings.TraitorsMinStartDelay, server.ServerSettings.TraitorsMaxStartDelay, (float)RandomDouble()); + } + + public void SkipStartDelay() + { + startCountdown = 0.01f; } public void Update(float deltaTime) @@ -134,7 +123,7 @@ namespace Barotrauma if (missionCompleted) { Missions.Clear(); - startCountdown = MathHelper.Lerp(server.ServerSettings.TraitorsMinRestartDelay, server.ServerSettings.TraitorsMaxRestartDelay, (float)Traitor.TraitorMission.RandomDouble()); + startCountdown = MathHelper.Lerp(server.ServerSettings.TraitorsMinRestartDelay, server.ServerSettings.TraitorsMaxRestartDelay, (float)RandomDouble()); } } else if (startCountdown > 0.0f && server.GameStarted) @@ -145,7 +134,7 @@ namespace Barotrauma int playerCharactersCount = server.ConnectedClients.Sum(client => client.Character != null && !client.Character.IsDead ? 1 : 0); if (playerCharactersCount < server.ServerSettings.TraitorsMinPlayerCount) { - startCountdown = MathHelper.Lerp(server.ServerSettings.TraitorsMinRestartDelay, server.ServerSettings.TraitorsMaxRestartDelay, (float)Traitor.TraitorMission.RandomDouble()); + startCountdown = MathHelper.Lerp(server.ServerSettings.TraitorsMinRestartDelay, server.ServerSettings.TraitorsMaxRestartDelay, (float)RandomDouble()); return; } if (GameMain.GameSession.Mission is CombatMission) @@ -184,7 +173,7 @@ namespace Barotrauma } } Missions.Clear(); - startCountdown = MathHelper.Lerp(server.ServerSettings.TraitorsMinRestartDelay, server.ServerSettings.TraitorsMaxRestartDelay, (float)Traitor.TraitorMission.RandomDouble()); + startCountdown = MathHelper.Lerp(server.ServerSettings.TraitorsMinRestartDelay, server.ServerSettings.TraitorsMaxRestartDelay, (float)RandomDouble()); } } } @@ -198,42 +187,5 @@ namespace Barotrauma return TextManager.JoinServerMessages("\n\n", Missions.Select(mission => mission.Value.GlobalEndMessage).ToArray()); } - - public static T WeightedRandom(IList collection, int startIndex, int count, Func random, Func readSelectedWeight, Action writeSelectedWeight, int entryWeight, int selectionWeight) where T : class - { - if (count <= 0) - { - return null; - } - var maxWeight = readSelectedWeight(collection[startIndex]); - var totalWeight = entryWeight + maxWeight; - for (var i = 1; i < count; ++i) - { - var weight = readSelectedWeight(collection[startIndex + i]); - maxWeight = Math.Max(maxWeight, weight); - totalWeight += weight; - } - maxWeight += entryWeight; - totalWeight = count * maxWeight - totalWeight; - var selected = random(totalWeight); - for(var i = 0; i < count; ++i) - { - var entry = collection[startIndex + i]; - var weight = readSelectedWeight(entry); - selected -= maxWeight; - selected += weight; - if (selected <= 0) - { - writeSelectedWeight(entry, weight + selectionWeight); - return entry; - } - } - return null; - } - - public static T WeightedRandom(IList collection, Func random, Func readSelectedWeight, Action writeSelectedWeight, int entryWeight, int selectionWeight) where T : class - { - return WeightedRandom(collection, 0, collection.Count, random, readSelectedWeight, writeSelectedWeight, entryWeight, selectionWeight); - } } } diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/TraitorMission.cs b/Barotrauma/BarotraumaServer/Source/Traitors/TraitorMission.cs index dbb5235db..730cadcde 100644 --- a/Barotrauma/BarotraumaServer/Source/Traitors/TraitorMission.cs +++ b/Barotrauma/BarotraumaServer/Source/Traitors/TraitorMission.cs @@ -16,23 +16,16 @@ namespace Barotrauma { public class TraitorMission { - private static System.Random random = null; - - public static void InitializeRandom() => random = new System.Random((int)DateTime.UtcNow.Ticks); - - // All traitor related functionality should use the following interface for generating random values - public static int Random(int n) => random.Next(n); - - // All traitor related functionality should use the following interface for generating random values - public static double RandomDouble() => random.NextDouble(); - private static string wordsTxt = Path.Combine("Content", "CodeWords.txt"); private readonly List allObjectives = new List(); private readonly List pendingObjectives = new List(); private readonly List completedObjectives = new List(); - public virtual bool IsCompleted => pendingObjectives.Count <= 0; + /// + /// Has the mission been completed (does not mean that the traitor necessarily won, the mission is considered completed if the traitor fails for whatever reason) + /// + public bool IsCompleted => pendingObjectives.Count <= 0; public readonly Dictionary Traitors = new Dictionary(); @@ -168,14 +161,8 @@ namespace Barotrauma { ++numCandidates; } - var selected = TraitorManager.WeightedRandom(availableCandidates, 0, numCandidates, Random, t => - { - var previousClient = server.FindPreviousClientData(t.Item1); - return Math.Max( - previousClient != null ? traitorManager.GetTraitorCount(previousClient) : 0, - traitorManager.GetTraitorCount(Tuple.Create(t.Item1.SteamID, t.Item1.Connection?.EndPointString ?? ""))); - }, (t, c) => { traitorManager.SetTraitorCount(Tuple.Create(t.Item1.SteamID, t.Item1.Connection?.EndPointString ?? ""), c); }, 2, 3); + var selected = ToolBox.SelectWeightedRandom(availableCandidates, availableCandidates.Select(c => Math.Max(c.Item1.RoundsSincePlayedAsTraitor, 0.1f)).ToList(), TraitorManager.Random); assignedCandidates.Add(Tuple.Create(currentRole, selected)); foreach (var candidate in roleCandidates.Values) { @@ -189,7 +176,7 @@ namespace Barotrauma return assignedCandidates; } - public virtual bool CanBeStarted(GameServer server, TraitorManager traitorManager, Character.TeamType team) + public bool CanBeStarted(GameServer server, TraitorManager traitorManager, Character.TeamType team) { foreach (var role in Roles) { @@ -202,7 +189,7 @@ namespace Barotrauma return AssignTraitors(server, traitorManager, team) != null; } - public virtual bool Start(GameServer server, TraitorManager traitorManager, Character.TeamType team) + public bool Start(GameServer server, TraitorManager traitorManager, Character.TeamType team) { var assignedCandidates = AssignTraitors(server, traitorManager, team); if (assignedCandidates == null) @@ -210,11 +197,17 @@ namespace Barotrauma return false; } + foreach (Client client in server.ConnectedClients) + { + client.RoundsSincePlayedAsTraitor++; + } + Traitors.Clear(); foreach (var candidate in assignedCandidates) { var traitor = new Traitor(this, candidate.Item1, candidate.Item2.Item1.Character); Traitors.Add(candidate.Item1, traitor); + candidate.Item2.Item1.RoundsSincePlayedAsTraitor = 0; } CodeWords = ToolBox.GetRandomLine(wordsTxt) + ", " + ToolBox.GetRandomLine(wordsTxt); CodeResponse = ToolBox.GetRandomLine(wordsTxt) + ", " + ToolBox.GetRandomLine(wordsTxt); @@ -250,15 +243,17 @@ namespace Barotrauma public delegate void TraitorWinHandler(); - public virtual void Update(float deltaTime, TraitorWinHandler winHandler) + public void Update(float deltaTime, TraitorWinHandler winHandler) { if (pendingObjectives.Count <= 0 || Traitors.Count <= 0) { return; } - if (Traitors.Values.Any(traitor => traitor.Character?.IsDead ?? true)) + if (Traitors.Values.Any(traitor => traitor.Character?.IsDead ?? true || traitor.Character.Removed)) { Traitors.Values.ForEach(traitor => traitor.UpdateCurrentObjective("", Identifier)); + pendingObjectives.Clear(); + Traitors.Clear(); return; } var startedObjectives = new List(); @@ -321,28 +316,69 @@ namespace Barotrauma } public delegate bool CharacterFilter(Character character); - public Character FindKillTarget(Character traitor, CharacterFilter filter) + public List FindKillTarget(Character traitor, CharacterFilter filter, int count = -1, float percentage = -1f) { if (traitor == null) { return null; } - List validCharacters = Character.CharacterList.FindAll(c => - c.TeamID == traitor.TeamID && - c != traitor && - !c.IsDead && - (filter == null || filter(c))); + List validCharacters = Character.CharacterList.FindAll(c => c.TeamID == traitor.TeamID && + c != traitor && !c.IsDead && + (filter == null || filter(c))); + + int targetCount = 1; + if (count > 0) + { + targetCount = count; + } + else if (percentage > 0f) + { + targetCount = (int)Math.Max(1, Math.Floor(validCharacters.Count * percentage)); + } + + List targetCharacters = new List(); if (validCharacters.Count > 0) { - return validCharacters[Random(validCharacters.Count)]; + for (int i = 0; i < targetCount; i++) + { + if (validCharacters.Count == 0) break; + Character character = validCharacters[TraitorManager.RandomInt(validCharacters.Count)]; + targetCharacters.Add(character); + validCharacters.Remove(character); + } + return targetCharacters; } #if ALLOW_SOLO_TRAITOR - return traitor; + targetCharacters.Add(traitor); + return targetCharacters; #else return null; #endif } + public string GetTargetNames(List targets) + { + string names = string.Empty; + for (int i = 0; i < targets.Count; i++) + { + names += targets[i].Name; + + if (i < targets.Count - 1) + { + names += ", "; + } + } + + if (names.Length > 0) + { + return names; + } + else + { + return TextManager.FormatServerMessage("unknown"); + } + } + public TraitorMission(string identifier, string startText, string globalEndMessageSuccessTextId, string globalEndMessageSuccessDeadTextId, string globalEndMessageSuccessDetainedTextId, string globalEndMessageFailureTextId, string globalEndMessageFailureDeadTextId, string globalEndMessageFailureDetainedTextId, IEnumerable> roles, ICollection objectives) { Identifier = identifier; diff --git a/Barotrauma/BarotraumaServer/Source/Traitors/TraitorMissionPrefab.cs b/Barotrauma/BarotraumaServer/Source/Traitors/TraitorMissionPrefab.cs index bd7d03f5f..758db65f8 100644 --- a/Barotrauma/BarotraumaServer/Source/Traitors/TraitorMissionPrefab.cs +++ b/Barotrauma/BarotraumaServer/Source/Traitors/TraitorMissionPrefab.cs @@ -12,12 +12,11 @@ namespace Barotrauma public class TraitorMissionEntry { public readonly TraitorMissionPrefab Prefab; - public int SelectedWeight; + public float SelectedWeight; public TraitorMissionEntry(XElement element) { Prefab = new TraitorMissionPrefab(element); - SelectedWeight = 0; } } public static readonly List List = new List(); @@ -39,7 +38,14 @@ namespace Barotrauma public static TraitorMissionPrefab RandomPrefab() { - return TraitorManager.WeightedRandom(List, Traitor.TraitorMission.Random, entry => entry.SelectedWeight, (entry, weight) => entry.SelectedWeight = weight, 2, 3)?.Prefab; + var selected = ToolBox.SelectWeightedRandom(List, List.Select(mission => Math.Max(mission.SelectedWeight, 0.1f)).ToList(), TraitorManager.Random); + //the weight of the missions that didn't get selected keeps growing the make them more likely to get picked + foreach (var mission in List) + { + mission.SelectedWeight += 10; + } + selected.SelectedWeight = 0.0f; + return selected.Prefab; } private class AttributeChecker : IDisposable @@ -113,15 +119,23 @@ namespace Barotrauma case "killtarget": { checker.Optional(targetFilters.Keys.ToArray()); - List filters = new List(); + checker.Optional("causeofdeath"); + checker.Optional("affliction"); + checker.Optional("roomname"); + checker.Optional("targetcount"); + checker.Optional("targetpercentage"); + List killFilters = new List(); foreach (var attribute in Config.Attributes()) { if (targetFilters.TryGetValue(attribute.Name.ToString().ToLower(System.Globalization.CultureInfo.InvariantCulture), out var filter)) { - filters.Add((character) => filter(attribute.Value, character)); + killFilters.Add((character) => filter(attribute.Value, character)); } } - goal = new Traitor.GoalKillTarget((character) => filters.All(f => f(character))); + goal = new Traitor.GoalKillTarget((character) => killFilters.All(f => f(character)), + (CauseOfDeathType)Enum.Parse(typeof(CauseOfDeathType), Config.GetAttributeString("causeofdeath", "Unknown"), true), + Config.GetAttributeString("affliction", null), Config.GetAttributeString("targethull", null), Config.GetAttributeInt("targetcount", -1), + Config.GetAttributeFloat("targetpercentage", -1f)); break; } case "destroyitems": @@ -157,8 +171,16 @@ namespace Barotrauma break; case "finditem": checker.Required("identifier"); - checker.Optional("preferNew", "allowNew", "allowExisting", "allowedContainers"); - goal = new Traitor.GoalFindItem(Config.GetAttributeString("identifier", null), Config.GetAttributeBool("preferNew", true), Config.GetAttributeBool("allowNew", true), Config.GetAttributeBool("allowExisting", true), Config.GetAttributeStringArray("allowedContainers", new string[] {"steelcabinet", "mediumsteelcabinet", "suppliescabinet"})); + checker.Optional("preferNew", "allowNew", "allowExisting", "allowedContainers", "percentage"); + List itemCountFilters = new List(); + foreach (var attribute in Config.Attributes()) + { + if (targetFilters.TryGetValue(attribute.Name.ToString().ToLower(System.Globalization.CultureInfo.InvariantCulture), out var filter)) + { + itemCountFilters.Add((character) => filter(attribute.Value, character)); + } + } + goal = new Traitor.GoalFindItem((character) => itemCountFilters.All(f => f(character)), Config.GetAttributeString("identifier", null), Config.GetAttributeBool("preferNew", true), Config.GetAttributeBool("allowNew", true), Config.GetAttributeBool("allowExisting", true), Config.GetAttributeFloat("percentage", -1f), Config.GetAttributeStringArray("allowedContainers", new string[] {"steelcabinet", "mediumsteelcabinet", "suppliescabinet"})); break; case "replaceinventory": checker.Required("containers", "replacements"); @@ -167,7 +189,39 @@ namespace Barotrauma break; case "reachdistancefromsub": checker.Optional("distance"); - goal = new Traitor.GoalReachDistanceFromSub(Config.GetAttributeFloat("distance", 10000.0f)); + goal = new Traitor.GoalReachDistanceFromSub(Config.GetAttributeFloat("distance", 125f)); + break; + case "injectpoison": + checker.Optional(targetFilters.Keys.ToArray()); + checker.Required("poison"); + checker.Required("affliction"); + checker.Optional("targetcount"); + checker.Optional("targetpercentage"); + List poisonFilters = new List(); + foreach (var attribute in Config.Attributes()) + { + if (targetFilters.TryGetValue(attribute.Name.ToString().ToLower(System.Globalization.CultureInfo.InvariantCulture), out var filter)) + { + poisonFilters.Add((character) => filter(attribute.Value, character)); + } + } + goal = new Traitor.GoalInjectTarget((character) => poisonFilters.All(f => f(character)), Config.GetAttributeString("poison", null), + Config.GetAttributeString("affliction", null), Config.GetAttributeInt("targetcount", -1), Config.GetAttributeFloat("targetpercentage", -1f)); + break; + case "unwire": + checker.Required("tag"); + checker.Optional("connectionname"); + checker.Optional("connectiondisplayname"); + goal = new Traitor.GoalUnwiring(Config.GetAttributeString("tag", null), Config.GetAttributeString("connectionname", null), Config.GetAttributeString("connectiondisplayname)", null)); + break; + case "transformentity": + checker.Required("entities", "entitytypes"); + checker.Optional("catalystid"); + goal = new Traitor.GoalEntityTransformation(Config.GetAttributeStringArray("entities", null), Config.GetAttributeStringArray("entitytypes", null), Config.GetAttributeString("catalystid", null)); + break; + case "keeptransformedalive": + checker.Required("speciesname"); + goal = new Traitor.GoalKeepTransformedAlive(Config.GetAttributeString("speciesname", null)); break; default: GameServer.Log($"Unrecognized goal type \"{goalType}\".", ServerLog.MessageType.Error); diff --git a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml index f1f352a9a..b0500a51c 100644 --- a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml +++ b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml @@ -58,6 +58,8 @@ + + @@ -81,6 +83,8 @@ + + diff --git a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Ragdolls/RedcrawlerDefaultRagdoll.xml b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Ragdolls/RedcrawlerDefaultRagdoll.xml index ac897c815..71035be33 100644 --- a/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Ragdolls/RedcrawlerDefaultRagdoll.xml +++ b/Barotrauma/BarotraumaShared/Mods/ExampleMod/Redcrawler/Ragdolls/RedcrawlerDefaultRagdoll.xml @@ -1,7 +1,7 @@  - + @@ -13,7 +13,7 @@ - + @@ -21,7 +21,7 @@ - + @@ -29,37 +29,37 @@ - + - + - + - + - + - + @@ -68,28 +68,28 @@ - + - + - + - + - + - + diff --git a/Barotrauma/BarotraumaShared/SharedCode.projitems b/Barotrauma/BarotraumaShared/SharedCode.projitems index c16a050b1..b7ffe114d 100644 --- a/Barotrauma/BarotraumaShared/SharedCode.projitems +++ b/Barotrauma/BarotraumaShared/SharedCode.projitems @@ -155,9 +155,11 @@ + + @@ -169,6 +171,7 @@ + @@ -275,6 +278,7 @@ + diff --git a/Barotrauma/BarotraumaShared/SharedContent.projitems b/Barotrauma/BarotraumaShared/SharedContent.projitems index a83760e28..b71d0d013 100644 --- a/Barotrauma/BarotraumaShared/SharedContent.projitems +++ b/Barotrauma/BarotraumaShared/SharedContent.projitems @@ -20,13 +20,9 @@ - - - - @@ -114,6 +110,174 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + PreserveNewest @@ -225,69 +389,6 @@ PreserveNewest - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - PreserveNewest @@ -333,6 +434,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -384,6 +488,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -393,9 +500,6 @@ PreserveNewest - - PreserveNewest - PreserveNewest @@ -459,6 +563,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -468,12 +575,39 @@ PreserveNewest + + PreserveNewest + PreserveNewest PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + PreserveNewest @@ -489,33 +623,118 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + PreserveNewest PreserveNewest + + PreserveNewest + PreserveNewest + + PreserveNewest + PreserveNewest + + PreserveNewest + PreserveNewest PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + PreserveNewest @@ -779,18 +998,45 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + PreserveNewest PreserveNewest + + PreserveNewest + Never PreserveNewest + + PreserveNewest + PreserveNewest @@ -848,6 +1094,33 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + PreserveNewest @@ -896,6 +1169,33 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + PreserveNewest @@ -1694,9 +1994,6 @@ PreserveNewest - - PreserveNewest - PreserveNewest @@ -2559,10 +2856,10 @@ PreserveNewest - Never + PreserveNewest - Never + PreserveNewest diff --git a/Barotrauma/BarotraumaShared/SharedContent.shproj.user b/Barotrauma/BarotraumaShared/SharedContent.shproj.user index 7e04c94d7..5bc13ae87 100644 --- a/Barotrauma/BarotraumaShared/SharedContent.shproj.user +++ b/Barotrauma/BarotraumaShared/SharedContent.shproj.user @@ -1,6 +1,6 @@  - false + true \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/AIController.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/AIController.cs index 80aa66cb1..f87c4be5e 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/AIController.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/AIController.cs @@ -1,8 +1,9 @@ using Microsoft.Xna.Framework; +using System.Collections.Generic; namespace Barotrauma { - public enum AIState { Idle, Attack, Escape, Eat } + public enum AIState { Idle, Attack, Escape, Eat, Flee } abstract partial class AIController : ISteerable { @@ -11,7 +12,11 @@ namespace Barotrauma public readonly Character Character; private AIState state; + private AIState previousState; + // Update only when the value changes, not when it keeps the same. + protected AITarget _lastAiTarget; + // Updated each time the value is updated (also when the value is the same). protected AITarget _previousAiTarget; protected AITarget _selectedAiTarget; public AITarget SelectedAiTarget @@ -21,6 +26,13 @@ namespace Barotrauma { _previousAiTarget = _selectedAiTarget; _selectedAiTarget = value; + if (_selectedAiTarget != _previousAiTarget) + { + if (_previousAiTarget != null) + { + _lastAiTarget = _previousAiTarget; + } + } } } @@ -72,16 +84,38 @@ namespace Barotrauma get { return state; } set { - if (state == value) return; + if (state == value) { return; } + previousState = state; OnStateChanged(state, value); state = value; } } + public AIState PreviousState => previousState; + + private IEnumerable visibleHulls; + private float hullVisibilityTimer; + const float hullVisibilityInterval = 0.5f; + public IEnumerable VisibleHulls + { + get + { + if (visibleHulls == null) + { + visibleHulls = Character.GetVisibleHulls(); + } + return visibleHulls; + } + private set + { + visibleHulls = value; + } + } + public AIController (Character c) { Character = c; - + hullVisibilityTimer = Rand.Range(0f, hullVisibilityTimer); Enabled = true; } @@ -89,7 +123,18 @@ namespace Barotrauma public virtual void SelectTarget(AITarget target) { } - public virtual void Update(float deltaTime) { } + public virtual void Update(float deltaTime) + { + if (hullVisibilityTimer > 0) + { + hullVisibilityTimer--; + } + else + { + hullVisibilityTimer = hullVisibilityInterval; + VisibleHulls = Character.GetVisibleHulls(); + } + } protected virtual void OnStateChanged(AIState from, AIState to) { } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/EnemyAIController.cs index e85432eef..e89761972 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/EnemyAIController.cs @@ -19,6 +19,7 @@ namespace Barotrauma /// public bool TargetOutposts; + // TODO: use a struct? class WallTarget { public Vector2 Position; @@ -37,6 +38,8 @@ namespace Barotrauma private const float RaycastInterval = 1.0f; + private float avoidLookAheadDistance; + private SteeringManager outsideSteering, insideSteering; private float updateTargetsTimer; @@ -81,11 +84,16 @@ namespace Barotrauma private Dictionary targetMemories; - private float colliderSize; + private float colliderWidth; + private float colliderLength; + private bool canAttackSub; // TODO: expose? private readonly float priorityFearIncreasement = 2; private readonly float memoryFadeTime = 0.5f; + private readonly float avoidTime = 3; + + private float avoidTimer; public LatchOntoAI LatchOntoAI { get; private set; } public SwarmBehavior SwarmBehavior { get; private set; } @@ -94,8 +102,8 @@ namespace Barotrauma { get { - var targetingPriority = GetTargetingPriority(Character.HumanSpeciesName); - return targetingPriority != null && targetingPriority.State == AIState.Attack && targetingPriority.Priority > 0.0f; + var target = GetTarget(Character.HumanSpeciesName); + return target != null && target.State == AIState.Attack && target.Priority > 0.0f; } } @@ -103,8 +111,8 @@ namespace Barotrauma { get { - var targetingPriority = GetTargetingPriority("room"); - return targetingPriority != null && targetingPriority.State == AIState.Attack && targetingPriority.Priority > 0.0f; + var target = GetTarget("room"); + return target != null && target.State == AIState.Attack && target.Priority > 0.0f; } } @@ -113,7 +121,7 @@ namespace Barotrauma get { //can't enter a submarine when attached to something - return LatchOntoAI == null || !LatchOntoAI.IsAttached; + return Character.AnimController.CanEnterSubmarine && (LatchOntoAI == null || !LatchOntoAI.IsAttached); } } @@ -121,8 +129,12 @@ namespace Barotrauma { get { - //can't flip when attached to something or when reversing - return !Reverse && (LatchOntoAI == null || !LatchOntoAI.IsAttached); + //can't flip when attached to something, when eating, or reversing or in a (relatively) small room + return !Reverse && + (State != AIState.Eat || Character.SelectedCharacter == null) && + (LatchOntoAI == null || !LatchOntoAI.IsAttached) && + (Character.CurrentHull == null || !Character.AnimController.InWater || Math.Min(Character.CurrentHull.Size.X, Character.CurrentHull.Size.Y) > ConvertUnits.ToDisplayUnits(Math.Max(colliderLength, colliderWidth))); + } } @@ -178,14 +190,14 @@ namespace Barotrauma } bool canBreakDoors = false; - if (GetTargetingPriority("room")?.Priority > 0.0f) + if (GetTarget("room")?.Priority > 0.0f) { - AttackContext currentContext = Character.GetAttackContext(); + var currentContexts = Character.GetAttackContexts(); foreach (Limb limb in Character.AnimController.Limbs) { if (limb.attack == null) { continue; } if (!limb.attack.IsValidTarget(AttackTarget.Structure)) { continue; } - if (limb.attack.IsValidContext(currentContext) && limb.attack.StructureDamage > 0.0f) + if (limb.attack.IsValidContext(currentContexts) && limb.attack.StructureDamage > 0.0f) { canBreakDoors = true; break; @@ -198,22 +210,17 @@ namespace Barotrauma steeringManager = outsideSteering; State = AIState.Idle; - colliderSize = 0.1f; - switch (Character.AnimController.Collider.BodyShape) - { - case PhysicsBody.Shape.Capsule: - case PhysicsBody.Shape.HorizontalCapsule: - case PhysicsBody.Shape.Circle: - colliderSize = Character.AnimController.Collider.radius * 2; - break; - case PhysicsBody.Shape.Rectangle: - colliderSize = Math.Min(Character.AnimController.Collider.width, Character.AnimController.Collider.height); - break; - } + var size = Character.AnimController.Collider.GetSize(); + colliderWidth = size.X; + colliderLength = size.Y; + + avoidLookAheadDistance = Math.Max(colliderWidth * 3, 1.5f); + + canAttackSub = Character.AnimController.CanAttackSubmarine; } private CharacterParams.AIParams AIParams => Character.Params.AI; - private CharacterParams.TargetParams GetTargetingPriority(string targetTag) => AIParams.GetTarget(targetTag, false); + private CharacterParams.TargetParams GetTarget(string targetTag) => AIParams.GetTarget(targetTag, false); public override void SelectTarget(AITarget target) => SelectTarget(target, 100); @@ -222,12 +229,12 @@ namespace Barotrauma SelectedAiTarget = target; selectedTargetMemory = GetTargetMemory(target); selectedTargetMemory.Priority = priority; - targetValue = priority; } public override void Update(float deltaTime) { if (DisableEnemyAI) { return; } + base.Update(deltaTime); bool ignorePlatforms = (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X)); if (steeringManager is IndoorsSteeringManager) @@ -263,7 +270,11 @@ namespace Barotrauma ignoredTargets.Clear(); targetIgnoreTimer = targetIgnoreTime; } - + avoidTimer -= deltaTime; + if (avoidTimer < 0) + { + avoidTimer = 0; + } UpdateTargetMemories(deltaTime); if (updateTargetsTimer > 0.0) { @@ -271,21 +282,20 @@ namespace Barotrauma } else { - UpdateTargets(Character, out CharacterParams.TargetParams targetingPriority); + UpdateTargets(Character, out CharacterParams.TargetParams targetingParams); updateTargetsTimer = UpdateTargetsInterval * Rand.Range(0.75f, 1.25f); - if (SelectedAiTarget == null) + if (avoidTimer > 0) + { + State = AIState.Escape; + } + else if (SelectedAiTarget == null) { State = AIState.Idle; } - else if (Character.HealthPercentage < FleeHealthThreshold && SwarmBehavior == null) + else if (targetingParams != null) { - // Don't flee from damage if in a swarm. - State = AIState.Escape; - } - else if (targetingPriority != null) - { - State = targetingPriority.State; + State = targetingParams.State; } } @@ -326,6 +336,7 @@ namespace Barotrauma UpdateEating(deltaTime); break; case AIState.Escape: + case AIState.Flee: run = true; UpdateEscape(deltaTime); break; @@ -348,31 +359,59 @@ namespace Barotrauma private void UpdateIdle(float deltaTime) { - if (Character.Submarine == null && - SimPosition.Y < ConvertUnits.ToSimUnits(Character.CharacterHealth.CrushDepth * 0.75f)) + var pathSteering = SteeringManager as IndoorsSteeringManager; + if (pathSteering == null) { - //steer straight up if very deep - steeringManager.SteeringManual(deltaTime, Vector2.UnitY); - return; + if (SimPosition.Y < ConvertUnits.ToSimUnits(Character.CharacterHealth.CrushDepth * 0.75f)) + { + //steer straight up if very deep + steeringManager.SteeringManual(deltaTime, Vector2.UnitY); + SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 1); + return; + } + SteerInsideLevel(deltaTime); } - - SteerInsideLevel(deltaTime); - - if (SelectedAiTarget != null && SelectedAiTarget.Entity.Submarine == Character.Submarine) + if (pathSteering == null && SelectedAiTarget?.Entity != null && SelectedAiTarget.Entity.Submarine == Character.Submarine) { // Steer towards the target Vector2 targetSimPos = Character.Submarine == null ? ConvertUnits.ToSimUnits(SelectedAiTarget.WorldPosition) : SelectedAiTarget.SimPosition; - steeringManager.SteeringAvoid(deltaTime, colliderSize * 3.0f); - steeringManager.SteeringSeek(targetSimPos); + steeringManager.SteeringSeek(targetSimPos, 5); + SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5); } else { - // Wander around randomly - if (Character.Submarine == null) + if (pathSteering != null) { - steeringManager.SteeringAvoid(deltaTime, colliderSize * 5.0f); + pathSteering.Wander(deltaTime, ConvertUnits.ToDisplayUnits(colliderLength), stayStillInTightSpace: false); + } + else + { + var target = SelectedAiTarget ?? _lastAiTarget; + if (target?.Entity != null && PreviousState == AIState.Attack) + { + var memory = GetTargetMemory(target); + if (memory != null) + { + var location = memory.Location; + var dist = Vector2.DistanceSquared(WorldPosition, location); + float minDist = 50; + if (dist < minDist * minDist) + { + // Target is gone + SelectedAiTarget = null; + _lastAiTarget = null; + } + else + { + steeringManager.SteeringSeek(Character.GetRelativeSimPosition(target.Entity, location), 5); + SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5); + return; + } + } + } + steeringManager.SteeringWander(); + SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5); } - steeringManager.SteeringWander(0.5f); } } @@ -400,26 +439,26 @@ namespace Barotrauma { if (gap.Submarine != Character.Submarine) { continue; } if (gap.Open < 1 || gap.IsRoomToRoom) { continue; } + 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; } + } var path = indoorSteering.PathFinder.FindPath(Character.SimPosition, gap.SimPosition, Character.Submarine); 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; } } } } - else + else if (Character.Submarine == null) { SteerInsideLevel(deltaTime); } if (escapePoint != Vector2.Zero && Vector2.DistanceSquared(Character.SimPosition, escapePoint) > 1) { - SteeringManager.SteeringSeek(escapePoint); + SteeringManager.SteeringSeek(escapePoint, 10); } else { @@ -429,7 +468,7 @@ namespace Barotrauma if (!MathUtils.IsValid(escapeDir)) escapeDir = Vector2.UnitY; SteeringManager.SteeringManual(deltaTime, escapeDir); SteeringManager.SteeringWander(); - SteeringManager.SteeringAvoid(deltaTime, colliderSize * 3.0f); + SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5); } } @@ -437,16 +476,19 @@ namespace Barotrauma #region Attack + private Vector2 attackWorldPos; + private Vector2 attackSimPos; + private void UpdateAttack(float deltaTime) { - if (SelectedAiTarget == null) + if (SelectedAiTarget == null || SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed) { State = AIState.Idle; return; } - Vector2 attackWorldPos = SelectedAiTarget.WorldPosition; - Vector2 attackSimPos = SelectedAiTarget.SimPosition; + attackWorldPos = SelectedAiTarget.WorldPosition; + attackSimPos = SelectedAiTarget.SimPosition; if (SelectedAiTarget.Entity is Item item) { @@ -475,23 +517,6 @@ namespace Barotrauma raycastTimer = RaycastInterval; } - if (SelectedAiTarget.Entity is Character c) - { - //target the closest limb if the target is a character - 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.WorldPosition, WorldPosition) / Math.Max(limb.AttackPriority, 0.1f); - if (dist < closestDist) - { - closestDist = dist; - attackWorldPos = limb.WorldPosition; - attackSimPos = limb.SimPosition; - } - } - } - if (wallTarget != null) { attackWorldPos = wallTarget.Position; @@ -503,32 +528,10 @@ namespace Barotrauma } 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; - } - else if (Character.Submarine != SelectedAiTarget.Entity.Submarine) - { - if (Character.Submarine != null && SelectedAiTarget.Entity.Submarine != null) - { - Vector2 diff = Character.Submarine.SimPosition - SelectedAiTarget.Entity.Submarine.SimPosition; - attackSimPos -= diff; - } - } + attackSimPos = Character.GetRelativeSimPosition(SelectedAiTarget.Entity); } - if (Math.Abs(Character.AnimController.movement.X) > 0.1f && !Character.AnimController.InWater && - (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer || Character.Controlled == Character)) - { - Character.AnimController.TargetDir = Character.WorldPosition.X < attackWorldPos.X ? Direction.Right : Direction.Left; - } - - if (AggressiveBoarding) + if (CanEnterSubmarine && Character.CurrentHull != null) { //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)) @@ -557,17 +560,55 @@ namespace Barotrauma else if (SelectedAiTarget.Entity is Item i) { var door = i.GetComponent(); - //steer through the door manually if it's open or broken + // Steer through the door manually if it's open or broken + // Don't try to enter dry hulls if cannot walk or if the gap is too narrow if (door?.LinkedGap?.FlowTargetHull != null && !door.LinkedGap.IsRoomToRoom && (door.IsOpen || door.Item.Condition <= 0.0f)) { - LatchOntoAI?.DeattachFromBody(); - Character.AnimController.ReleaseStuckLimbs(); - var velocity = Vector2.Normalize(door.LinkedGap.FlowTargetHull.WorldPosition - Character.WorldPosition); - steeringManager.SteeringManual(deltaTime, velocity); - return; + if (Character.AnimController.CanWalk || door.LinkedGap.FlowTargetHull.WaterPercentage > 25) + { + if (door.LinkedGap.Size > ConvertUnits.ToDisplayUnits(colliderWidth)) + { + LatchOntoAI?.DeattachFromBody(); + Character.AnimController.ReleaseStuckLimbs(); + var velocity = Vector2.Normalize(door.LinkedGap.FlowTargetHull.WorldPosition - Character.WorldPosition); + steeringManager.SteeringManual(deltaTime, velocity); + return; + } + } } } } + else if (SelectedAiTarget.Entity is Structure w && wallTarget == null) + { + // Targeting only the outer walls + bool isBroken = true; + for (int i = 0; i < w.Sections.Length; i++) + { + if (!w.SectionBodyDisabled(i)) + { + isBroken = false; + Vector2 sectionPos = w.SectionPosition(i); + attackWorldPos = sectionPos; + if (w.Submarine != null) + { + attackWorldPos += w.Submarine.Position; + } + attackSimPos = ConvertUnits.ToSimUnits(attackWorldPos); + break; + } + } + if (isBroken) + { + IgnoreTarget(SelectedAiTarget); + State = AIState.Idle; + } + } + + if (Math.Abs(Character.AnimController.movement.X) > 0.1f && !Character.AnimController.InWater && + (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer || Character.Controlled == Character)) + { + Character.AnimController.TargetDir = Character.WorldPosition.X < attackWorldPos.X ? Direction.Right : Direction.Left; + } bool canAttack = true; if (IsCoolDownRunning) @@ -693,64 +734,58 @@ namespace Barotrauma attackVector = null; } - if (!CanAttack()) - { - // Invalid target - State = AIState.Idle; - IgnoreTarget(SelectedAiTarget); - return; - } - if (canAttack) { if (AttackingLimb == null || _previousAiTarget != SelectedAiTarget) { AttackingLimb = GetAttackLimb(attackWorldPos); } - if (AttackingLimb == null) + canAttack = AttackingLimb != null && AttackingLimb.attack.CoolDownTimer <= 0; + } + if (!canAttack && wallTarget != null && SelectedAiTarget.Entity.Submarine != null && !canAttackSub) + { + // Steer towards the target, but turn away if a wall is blocking the way + float d = ConvertUnits.ToDisplayUnits(colliderLength) * 3; + if (Vector2.DistanceSquared(Character.AnimController.MainLimb.WorldPosition, attackWorldPos) < d * d) { - if (wallTarget != null) - { - float d = ConvertUnits.ToDisplayUnits(colliderSize) * 10; - if (Vector2.DistanceSquared(Character.AnimController.MainLimb.WorldPosition, attackWorldPos) < d * d) - { - // No valid attack limb -> let's turn away - State = AIState.Idle; - IgnoreTarget(SelectedAiTarget); - return; - } - } - canAttack = false; - } - else - { - canAttack = AttackingLimb.attack.CoolDownTimer <= 0; + State = AIState.Idle; + IgnoreTarget(SelectedAiTarget); + return; } } float distance = 0; Limb attackTargetLimb = null; + Character targetCharacter = SelectedAiTarget.Entity as Character; if (canAttack) { - if (SelectedAiTarget.Entity is Character targetCharacter) + // Target a specific limb instead of the target center position + if (wallTarget == null && targetCharacter != null) { var targetLimbType = AttackingLimb.Params.Attack.Attack.TargetLimbType; - if (targetLimbType != LimbType.None) + attackTargetLimb = GetTargetLimb(AttackingLimb, targetCharacter, targetLimbType); + if (attackTargetLimb == null) { - attackTargetLimb = GetTargetLimb(AttackingLimb, targetLimbType, targetCharacter); - if (attackTargetLimb == null) - { - State = AIState.Idle; - return; - } - attackWorldPos = attackTargetLimb.WorldPosition; + State = AIState.Idle; + IgnoreTarget(SelectedAiTarget); + return; } + attackWorldPos = attackTargetLimb.WorldPosition; + attackSimPos = Character.GetRelativeSimPosition(attackTargetLimb); } // Check that we can reach the target Vector2 toTarget = attackWorldPos - AttackingLimb.WorldPosition; - if (SelectedAiTarget.Entity is Character targetC) + if (wallTarget != null) + { + if (wallTarget.Structure.Submarine != null) + { + Vector2 margin = CalculateMargin(wallTarget.Structure.Submarine.Velocity); + toTarget += margin; + } + } + else if (targetCharacter != null) { // Add a margin when the target is moving away, because otherwise it might be difficult to reach it (the attack takes some time to perform) - Vector2 margin = CalculateMargin(targetC.AnimController.Collider.LinearVelocity); + Vector2 margin = CalculateMargin(targetCharacter.AnimController.Collider.LinearVelocity); toTarget += margin; } else if (SelectedAiTarget.Entity is MapEntity e) @@ -764,6 +799,7 @@ namespace Barotrauma Vector2 CalculateMargin(Vector2 targetVelocity) { + if (targetVelocity == Vector2.Zero) { return targetVelocity; } float dot = Vector2.Dot(Vector2.Normalize(targetVelocity), Vector2.Normalize(Character.AnimController.Collider.LinearVelocity)); return ConvertUnits.ToDisplayUnits(targetVelocity) * AttackingLimb.attack.Duration * dot; } @@ -811,40 +847,51 @@ namespace Barotrauma 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 pathSteering) { - if (indoorsSteering.CurrentPath != null && !indoorsSteering.IsPathDirty) + if (pathSteering.CurrentPath != null) { - if (indoorsSteering.CurrentPath.Unreachable) + // Attack doors + if (canAttackSub && pathSteering.CurrentPath.CurrentNode?.ConnectedDoor != null && SelectedAiTarget != pathSteering.CurrentPath.CurrentNode.ConnectedDoor.Item.AiTarget) { - if (selectedTargetMemory != null) + SelectTarget(pathSteering.CurrentPath.CurrentNode.ConnectedDoor.Item.AiTarget); + return; + } + else if (canAttackSub && pathSteering.CurrentPath.NextNode?.ConnectedDoor != null && SelectedAiTarget != pathSteering.CurrentPath.NextNode.ConnectedDoor.Item.AiTarget) + { + SelectTarget(pathSteering.CurrentPath.NextNode.ConnectedDoor.Item.AiTarget); + return; + } + else + { + // Steer towards the target if in the same room and swimming + if (Character.AnimController.InWater && targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull)) { - //wander around randomly and decrease the priority faster if no path is found - selectedTargetMemory.Priority -= deltaTime * memoryFadeTime * 10; + SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(attackSimPos - steeringLimb.SimPosition)); + } + else + { + SteeringManager.SteeringSeek(steerPos, 2); + // Switch to Idle when cannot reach the target and if cannot damage the walls + if ((!canAttackSub || wallTarget == null) && !pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable) + { + State = AIState.Idle; + return; + } } - SteeringManager.SteeringWander(); - } - else if (indoorsSteering.CurrentPath.Finished) - { - SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(attackSimPos - steeringLimb.SimPosition)); - } - else if (indoorsSteering.CurrentPath.CurrentNode?.ConnectedDoor != null) - { - wallTarget = null; - SelectedAiTarget = indoorsSteering.CurrentPath.CurrentNode.ConnectedDoor.Item.AiTarget; - } - else if (indoorsSteering.CurrentPath.NextNode?.ConnectedDoor != null) - { - wallTarget = null; - SelectedAiTarget = indoorsSteering.CurrentPath.NextNode.ConnectedDoor.Item.AiTarget; } } + else + { + SteeringManager.SteeringSeek(steerPos, 10); + SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5); + } } - else if (Character.CurrentHull == null) + else { - SteeringManager.SteeringAvoid(deltaTime, colliderSize * 1.5f); + SteeringManager.SteeringSeek(steerPos, 10); + SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5); } if (canAttack) @@ -887,9 +934,6 @@ namespace Barotrauma return false; } - - private bool CanAttack() => CanAttack(wallTarget != null ? wallTarget.Structure : SelectedAiTarget?.Entity); - private bool CanAttack(Entity target) { if (target == null) { return false; } @@ -905,7 +949,7 @@ namespace Barotrauma private Limb GetAttackLimb(Vector2 attackWorldPos, Limb ignoredLimb = null) { - AttackContext currentContext = Character.GetAttackContext(); + var currentContexts = Character.GetAttackContexts(); Entity target = wallTarget != null ? wallTarget.Structure : SelectedAiTarget?.Entity; if (!CanAttack(target)) { return null; } Limb selectedLimb = null; @@ -917,7 +961,7 @@ namespace Barotrauma var attack = limb.attack; if (attack == null) { continue; } if (attack.CoolDownTimer > 0) { continue; } - if (!attack.IsValidContext(currentContext)) { continue; } + if (!attack.IsValidContext(currentContexts)) { continue; } if (!attack.IsValidTarget(target)) { continue; } if (target is ISerializableEntity se && target is Character) { @@ -973,9 +1017,8 @@ namespace Barotrauma { if (wall.SectionBodyDisabled(i)) { - if (AggressiveBoarding && CanPassThroughHole(wall, i)) + if (CanEnterSubmarine && CanPassThroughHole(wall, i)) { - //aggressive boarders always target holes they can pass through sectionIndex = i; break; } @@ -985,7 +1028,10 @@ namespace Barotrauma continue; } } - if (wall.SectionDamage(i) > sectionDamage) sectionIndex = i; + if (wall.SectionDamage(i) > sectionDamage) + { + sectionIndex = i; + } } Vector2 sectionPos = wall.SectionPosition(sectionIndex); @@ -1036,14 +1082,30 @@ namespace Barotrauma LatchOntoAI?.DeattachFromBody(); Character.AnimController.ReleaseStuckLimbs(); + if (Character.HealthPercentage <= FleeHealthThreshold) + { + State = AIState.Flee; + SelectedAiTarget = null; + wallTarget = null; + return; + } + if (attacker == null || attacker.AiTarget == null) { return; } 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 && Character.Params.AI.RetaliateWhenTakingDamage) + bool retaliate = attacker.Submarine == Character.Submarine && SelectedAiTarget != attacker.AiTarget; + bool avoidGunFire = attacker.Submarine != Character.Submarine && Character.Params.AI.AvoidGunfire; + if (State == AIState.Attack && !IsCoolDownRunning) { + // Don't retaliate or escape while performing an attack + retaliate = false; + avoidGunFire = false; + } + if (retaliate) + { + // Reduce the cooldown so that the character can react foreach (var limb in Character.AnimController.Limbs) { if (limb.attack != null) @@ -1052,6 +1114,10 @@ namespace Barotrauma } } } + else if (avoidGunFire) + { + avoidTimer = avoidTime * Rand.Range(0.75f, 1.25f); + } } // 10 dmg, 100 health -> 0.1 @@ -1059,7 +1125,7 @@ namespace Barotrauma private bool UpdateLimbAttack(float deltaTime, Limb attackingLimb, Vector2 attackSimPos, float distance = -1, Limb targetLimb = null) { - if (SelectedAiTarget == null) { return false; } + if (SelectedAiTarget?.Entity == null) { return false; } if (wallTarget != null) { // If the selected target is not the wall target, make the wall target the selected target. @@ -1093,6 +1159,7 @@ namespace Barotrauma { if (attackVector == null) { + // TODO: test adding some random variance here? attackVector = attackWorldPos - WorldPosition; } Vector2 attackDir = Vector2.Normalize(followThrough ? attackVector.Value : -attackVector.Value); @@ -1101,7 +1168,7 @@ namespace Barotrauma attackDir = Vector2.UnitY; } steeringManager.SteeringManual(deltaTime, attackDir); - steeringManager.SteeringAvoid(deltaTime, colliderSize * 3.0f); + SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 1); } #endregion @@ -1115,30 +1182,36 @@ namespace Barotrauma State = AIState.Idle; return; } - Limb mouthLimb = Character.AnimController.GetLimb(LimbType.Head); - if (mouthLimb == null) + if (SelectedAiTarget.Entity is Character target) { - DebugConsole.ThrowError("Character \"" + Character.SpeciesName + "\" failed to eat a target (No head limb defined)"); - State = AIState.Idle; - return; - } - Vector2 mouthPos = Character.AnimController.GetMouthPosition().Value; - Vector2 attackSimPosition = Character.Submarine == null ? ConvertUnits.ToSimUnits(SelectedAiTarget.WorldPosition) : SelectedAiTarget.SimPosition; - Vector2 limbDiff = attackSimPosition - mouthPos; - float limbDist = limbDiff.Length(); - if (limbDist < 2.0f) - { - if (SelectedAiTarget.Entity is Character c) + Limb mouthLimb = Character.AnimController.GetLimb(LimbType.Head); + if (mouthLimb == null) { - // TODO: what if we use this for eating something else than characters? - Character.SelectCharacter(c); + DebugConsole.ThrowError("Character \"" + Character.SpeciesName + "\" failed to eat a target (No head limb defined)"); + State = AIState.Idle; + return; + } + Vector2 mouthPos = Character.AnimController.GetMouthPosition().Value; + Vector2 attackSimPosition = Character.GetRelativeSimPosition(target); + Vector2 limbDiff = attackSimPosition - mouthPos; + float limbDist = limbDiff.LengthSquared(); + if (limbDist < 2 * 2) + { + Character.SelectCharacter(target); + steeringManager.SteeringManual(deltaTime, Vector2.Normalize(limbDiff) * 3); + Character.AnimController.Collider.ApplyForce(limbDiff * mouthLimb.Mass * 50.0f, mouthPos); + } + else + { + //steeringManager.SteeringSeek(attackSimPosition - (mouthPos - SimPosition), 2); + steeringManager.SteeringSeek(attackSimPosition + limbDiff, 2); + SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 1); } - steeringManager.SteeringManual(deltaTime, Vector2.Normalize(limbDiff)); - Character.AnimController.Collider.ApplyForce(limbDiff * mouthLimb.Mass * 50.0f, mouthPos); } else { - steeringManager.SteeringSeek(attackSimPosition - (mouthPos - SimPosition), 2); + IgnoreTarget(SelectedAiTarget); + State = AIState.Idle; } } @@ -1150,7 +1223,7 @@ namespace Barotrauma //goes through all the AItargets, evaluates how preferable it is to attack the target, //whether the Character can see/hear the target and chooses the most preferable target within //sight/hearing range - public AITarget UpdateTargets(Character character, out CharacterParams.TargetParams priority) + public AITarget UpdateTargets(Character character, out CharacterParams.TargetParams targetingParams) { if ((SelectedAiTarget != null || wallTarget != null) && IsLatchedOnSub) { @@ -1181,50 +1254,44 @@ namespace Barotrauma { // If attached to a valid target, just keep the target. // Priority not used in this case. - priority = null; + targetingParams = null; return SelectedAiTarget; } } AITarget newTarget = null; - priority = null; + targetValue = 0; selectedTargetMemory = null; - targetValue = 0.0f; + targetingParams = null; - foreach (AITarget target in AITarget.List) + foreach (AITarget aiTarget in AITarget.List) { - if (!target.Enabled) {continue; } - // Only ignore targets that are not in the same sub. - if (ignoredTargets.Contains(target) && target.Entity.Submarine != character.Submarine) { continue; } - if (Level.Loaded != null && target.WorldPosition.Y > Level.Loaded.Size.Y) + if (!aiTarget.Enabled) { continue; } + if (ignoredTargets.Contains(aiTarget)) { continue; } + if (Level.Loaded != null && aiTarget.WorldPosition.Y > Level.Loaded.Size.Y) { continue; } - if (target.Type == AITarget.TargetType.HumanOnly) { continue; } + if (aiTarget.Type == AITarget.TargetType.HumanOnly) { continue; } if (!TargetOutposts) { - if (target.Entity.Submarine != null && target.Entity.Submarine.IsOutpost) { continue; } + if (aiTarget.Entity.Submarine != null && aiTarget.Entity.Submarine.IsOutpost) { continue; } } - Character targetCharacter = target.Entity as Character; + Character targetCharacter = aiTarget.Entity as Character; //ignore the aitarget if it is the Character itself - if (targetCharacter == character) continue; + if (targetCharacter == character) { continue; } float valueModifier = 1; string targetingTag = null; if (targetCharacter != null) { + if (targetCharacter.Submarine != Character.Submarine) + { + // In a different sub or the target is outside when we are inside or vice versa. + continue; + } if (targetCharacter.IsDead) { 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) { @@ -1241,28 +1308,15 @@ namespace Barotrauma { targetingTag = "weaker"; } - if (State == AIState.Escape && targetingTag == "stronger") + if (targetingTag == "stronger" && State == AIState.Escape && SelectedAiTarget.Entity is Character c && c.AIController is EnemyAIController) { // 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 && !AggressiveBoarding) + else if (targetCharacter.CurrentHull != null && character.CurrentHull == null) { - //target inside, AI outside -> we'll be attacking a wall between the characters so use the priority for attacking rooms + // the character is inside and we're outside. targetingTag = "room"; } else if (AIParams.Targets.Any(t => t.Tag.Equals(targetCharacter.SpeciesName, StringComparison.OrdinalIgnoreCase))) @@ -1270,16 +1324,16 @@ namespace Barotrauma targetingTag = targetCharacter.SpeciesName.ToLowerInvariant(); } } - else if (target.Entity != null) + else if (aiTarget.Entity != null) { // Ignore the target if it's a room and the character is already inside a sub - if (character.CurrentHull != null && target.Entity is Hull) { continue; } + if (character.CurrentHull != null && aiTarget.Entity is Hull) { continue; } Door door = null; - if (target.Entity is Item item) + if (aiTarget.Entity is Item item) { //item inside and we're outside -> attack the hull - if (item.CurrentHull != null && character.CurrentHull == null && !AggressiveBoarding) + if (item.CurrentHull != null && character.CurrentHull == null) { targetingTag = "room"; } @@ -1300,7 +1354,7 @@ namespace Barotrauma continue; } } - else if (target.Entity is Structure s) + else if (aiTarget.Entity is Structure s) { targetingTag = "wall"; if (!s.HasBody) @@ -1314,61 +1368,61 @@ namespace Barotrauma } if (character.CurrentHull != null) { - // Ignore walls when inside. + // Ignore walls when inside (walltargets still work) continue; } valueModifier = 1; - float wallMaxHealth = 400; // Anything more than this is ignored -> 200 = 1 - // Prefer weaker targets. - valueModifier *= MathHelper.Lerp(1.5f, 0.5f, MathUtils.InverseLerp(0, 1, s.Health / wallMaxHealth)); - bool canAttackSub = Character.AnimController.CanAttackSubmarine; - if (!AggressiveBoarding) + if (!Character.AnimController.CanEnterSubmarine) { // Ignore disabled walls - bool isDisabled = true; - for (int i = 0; i < s.Sections.Length; i++) + bool isBroken = false; + if (!isBroken) { - if (!s.SectionBodyDisabled(i)) + for (int i = 0; i < s.Sections.Length; i++) { - isDisabled = false; - break; + if (!s.SectionBodyDisabled(i)) + { + isBroken = false; + break; + } } } - if (isDisabled) + if (isBroken) { continue; } } - //var hulls = s.Submarine.GetHulls(false); for (int i = 0; i < s.Sections.Length; i++) { var section = s.Sections[i]; - if (section.gap != null) + if (section.gap == null) { continue; } + bool leadsInside = !section.gap.IsRoomToRoom && section.gap.FlowTargetHull != null; + if (Character.AnimController.CanEnterSubmarine) { - if (AggressiveBoarding) + if (CanPassThroughHole(s, i)) { - if (CanPassThroughHole(s, i)) + valueModifier *= leadsInside ? (AggressiveBoarding ? 5 : 1) : 0; + } + else + { + // Ignore holes that cannot be passed through if cannot attack items/structures. Holes that are big enough should be targeted, so that we can get in + if (!canAttackSub) { - bool leadsInside = !section.gap.IsRoomToRoom && section.gap.FlowTargetHull != null; // hulls.Any(h => h.Rect.Intersects(section.rect) - valueModifier *= leadsInside ? 5 : 0; + valueModifier = 0; + break; } - else + if (AggressiveBoarding) { - // Ignore holes that cannot be passed through if cannot attack items/structures. Holes that are big enough should be targeted, so that we can get in if we are aggressive boarders - if (!canAttackSub) - { - valueModifier = 0; - break; - } // Up to 100% priority increase for every gap in the wall valueModifier *= 1 + section.gap.Open; } } - else - { - bool leadsInside = !section.gap.IsRoomToRoom && section.gap.FlowTargetHull != null; - valueModifier *= leadsInside ? 1 : 0; - } + } + else if (!leadsInside) + { + // Ignore inner walls + valueModifier = 0; + break; } } } @@ -1383,9 +1437,10 @@ namespace Barotrauma { targetingTag = "door"; } + if (door.Item.Submarine == null) { continue;} bool isOutdoor = door.LinkedGap?.FlowTargetHull != null && !door.LinkedGap.IsRoomToRoom; bool isOpen = door.IsOpen || door.Item.Condition <= 0.0f; - if (!isOpen && (!Character.AnimController.CanAttackSubmarine)) + if (!isOpen && (!canAttackSub)) { // Ignore doors that are not open if cannot attack items/structures. Open doors should be targeted, so that we can get in if we are aggressive boarders valueModifier = 0; @@ -1406,40 +1461,41 @@ namespace Barotrauma valueModifier *= isOpen ? 0 : 1; } } - else if (isOpen) //ignore broken and open doors + else if (!Character.AnimController.CanEnterSubmarine && isOpen) //ignore broken and open doors { continue; } } - else if (target.Entity is IDamageable targetDamageable && targetDamageable.Health <= 0.0f) + else if (aiTarget.Entity is IDamageable targetDamageable && targetDamageable.Health <= 0.0f) { continue; } } - if (targetingTag == null) continue; - var targetPrio = GetTargetingPriority(targetingTag); - if (targetPrio == null) { continue; } - valueModifier *= targetPrio.Priority; + if (targetingTag == null) { continue; } + var targetParams = GetTarget(targetingTag); + if (targetParams == null) { continue; } + valueModifier *= targetParams.Priority; - if (valueModifier == 0.0f) continue; + if (valueModifier == 0.0f) { continue; } - Vector2 toTarget = target.WorldPosition - character.WorldPosition; + Vector2 toTarget = aiTarget.WorldPosition - character.WorldPosition; 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) - if (targetMemories.ContainsKey(target)) dist *= 0.5f; + if (targetMemories.ContainsKey(aiTarget)) + { + dist *= 0.9f; + } - //ignore target if it's too far to see or hear - if (dist > target.SightRange * Sight && dist > target.SoundRange * Hearing) continue; - if (!target.IsWithinSector(WorldPosition)) continue; + if (!CanPerceive(aiTarget, dist)) { continue; } + if (!aiTarget.IsWithinSector(WorldPosition)) { continue; } //if the target is very close, the distance doesn't make much difference // -> just ignore the distance and attack whatever has the highest priority dist = Math.Max(dist, 100.0f); - AITargetMemory targetMemory = GetTargetMemory(target); + AITargetMemory targetMemory = GetTargetMemory(aiTarget); 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. @@ -1449,10 +1505,10 @@ namespace Barotrauma if (valueModifier > targetValue) { - newTarget = target; + newTarget = aiTarget; selectedTargetMemory = targetMemory; - priority = GetTargetingPriority(targetingTag); targetValue = valueModifier; + targetingParams = GetTarget(targetingTag); } } @@ -1468,24 +1524,52 @@ namespace Barotrauma { if (!targetMemories.TryGetValue(target, out AITargetMemory memory)) { - memory = new AITargetMemory(10); + memory = new AITargetMemory(target, 10); targetMemories.Add(target, memory); } return memory; } - private List removals = new List(); + private readonly List removals = new List(); private void UpdateTargetMemories(float deltaTime) { - removals.Clear(); - foreach (var memory in targetMemories) + if (_selectedAiTarget != null) { - // 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 (_selectedAiTarget.Entity == null || _selectedAiTarget.Entity.Removed) { - removals.Add(memory.Key); + _selectedAiTarget = null; + } + else + { + if (CanPerceive(_selectedAiTarget, distSquared: Vector2.DistanceSquared(Character.WorldPosition, _selectedAiTarget.WorldPosition))) + { + var memory = GetTargetMemory(_selectedAiTarget); + memory.Location = _selectedAiTarget.WorldPosition; + } + } + } + removals.Clear(); + foreach (var kvp in targetMemories) + { + var target = kvp.Key; + var memory = kvp.Value; + // Slowly decrease all memories + float fadeTime = memoryFadeTime; + if (target == SelectedAiTarget) + { + // Don't decrease the current memory + fadeTime = 0; + } + else if (target == _lastAiTarget) + { + // Halve the latest memory fading. + fadeTime /= 2; + } + memory.Priority -= fadeTime * deltaTime; + // Remove targets that have no priority or have been removed + if (memory.Priority <= 1 || target.Entity == null || target.Entity.Removed || !AITarget.List.Contains(target)) + { + removals.Add(target); } } removals.ForEach(r => targetMemories.Remove(r)); @@ -1496,6 +1580,7 @@ namespace Barotrauma private readonly HashSet ignoredTargets = new HashSet(); public void IgnoreTarget(AITarget target) { + if (target == null) { return; } ignoredTargets.Add(target); targetIgnoreTimer = targetIgnoreTime; } @@ -1511,6 +1596,18 @@ namespace Barotrauma AttackingLimb = null; } + private bool CanPerceive(AITarget target, float dist = -1, float distSquared = -1) + { + if (distSquared > -1) + { + return distSquared <= MathUtils.Pow(target.SightRange * Sight, 2) || distSquared <= MathUtils.Pow(target.SoundRange * Hearing, 2); + } + else + { + return dist <= target.SightRange * Sight || dist <= target.SoundRange * Hearing; + } + } + private void SteerInsideLevel(float deltaTime) { if (Level.Loaded == null) { return; } @@ -1521,7 +1618,7 @@ namespace Barotrauma float margin = 10.0f; - if (SimPosition.Y < 0.0f) + if (SimPosition.Y < 0.0) { steeringManager.SteeringManual(deltaTime, Vector2.UnitY * MathUtils.InverseLerp(0.0f, -margin, SimPosition.Y)); } @@ -1537,7 +1634,7 @@ namespace Barotrauma private int GetMinimumPassableHoleCount() { - return (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderSize) / Structure.WallSectionSize); + return (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderWidth) / Structure.WallSectionSize); } private bool CanPassThroughHole(Structure wall, int sectionIndex) @@ -1564,11 +1661,12 @@ namespace Barotrauma } private List targetLimbs = new List(); - public Limb GetTargetLimb(Limb attackLimb, LimbType targetLimbType, Character target) + public Limb GetTargetLimb(Limb attackLimb, Character target, LimbType targetLimbType = LimbType.None) { targetLimbs.Clear(); foreach (var limb in target.AnimController.Limbs) { + if (limb.IsSevered) { continue; } if (limb.type == targetLimbType || targetLimbType == LimbType.None) { targetLimbs.Add(limb); @@ -1576,12 +1674,21 @@ namespace Barotrauma } if (targetLimbs.None()) { - // If no limbs of given type was found, accept any limb + // If no limbs of given type was found, accept any limb. targetLimbs.AddRange(target.AnimController.Limbs); } - targetLimbs.Sort((limb1, limb2) => Vector2.DistanceSquared(limb1.WorldPosition, attackLimb.WorldPosition) - .CompareTo(Vector2.DistanceSquared(limb2.WorldPosition, attackLimb.WorldPosition))); - return targetLimbs.FirstOrDefault(); + float closestDist = float.MaxValue; + Limb targetLimb = null; + foreach (Limb limb in targetLimbs) + { + float dist = Vector2.DistanceSquared(limb.WorldPosition, attackLimb.WorldPosition) / Math.Max(limb.AttackPriority, 0.1f); + if (dist < closestDist) + { + closestDist = dist; + targetLimb = limb; + } + } + return targetLimb; } } @@ -1591,6 +1698,9 @@ namespace Barotrauma //and if the target attacks the Character, the priority increases) class AITargetMemory { + public readonly AITarget Target; + public Vector2 Location { get; set; } + private float priority; public float Priority @@ -1599,8 +1709,10 @@ namespace Barotrauma set { priority = MathHelper.Clamp(value, 1.0f, 100.0f); } } - public AITargetMemory(float priority) + public AITargetMemory(AITarget target, float priority) { + Target = target; + Location = target.WorldPosition; this.priority = priority; } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/HumanAIController.cs index 117f28a91..feb1411d7 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/HumanAIController.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; using Barotrauma.Extensions; +using Barotrauma.Items.Components; namespace Barotrauma { @@ -16,16 +17,20 @@ namespace Barotrauma private float sortTimer; private float crouchRaycastTimer; private float reactTimer; - private float hullVisibilityTimer; + private float unreachableClearTimer; private bool shouldCrouch; const float reactionTime = 0.5f; - const float hullVisibilityInterval = 0.5f; const float crouchRaycastInterval = 1; const float sortObjectiveInterval = 1; + const float clearUnreachableInterval = 30; + + private float flipTimer; + private const float FlipInterval = 0.5f; public static float HULL_SAFETY_THRESHOLD = 50; + public HashSet UnreachableHulls { get; private set; } = new HashSet(); public HashSet UnsafeHulls { get; private set; } = new HashSet(); private SteeringManager outsideSteering, insideSteering; @@ -50,23 +55,6 @@ namespace Barotrauma private set; } - private IEnumerable visibleHulls; - public IEnumerable VisibleHulls - { - get - { - if (visibleHulls == null) - { - visibleHulls = Character.GetVisibleHulls(); - } - return visibleHulls; - } - private set - { - visibleHulls = value; - } - } - public HumanAIController(Character c) : base(c) { if (!c.IsHuman) @@ -78,7 +66,6 @@ namespace Barotrauma objectiveManager = new AIObjectiveManager(c); reactTimer = Rand.Range(0f, reactionTime); sortTimer = Rand.Range(0f, sortObjectiveInterval); - hullVisibilityTimer = Rand.Range(0f, hullVisibilityTimer); InitProjSpecific(); } partial void InitProjSpecific(); @@ -86,6 +73,17 @@ namespace Barotrauma public override void Update(float deltaTime) { if (DisableCrewAI || Character.IsUnconscious || Character.Removed) { return; } + base.Update(deltaTime); + + if (unreachableClearTimer > 0) + { + unreachableClearTimer -= deltaTime; + } + else + { + unreachableClearTimer = clearUnreachableInterval; + UnreachableHulls.Clear(); + } float maxDistanceToSub = 3000; if (Character.Submarine != null || SelectedAiTarget?.Entity?.Submarine != null && @@ -110,17 +108,6 @@ namespace Barotrauma CheckCrouching(deltaTime); Character.ClearInputs(); - if (hullVisibilityTimer > 0) - { - hullVisibilityTimer--; - } - else - { - hullVisibilityTimer = hullVisibilityInterval; - VisibleHulls = Character.GetVisibleHulls(); - } - - objectiveManager.UpdateObjectives(deltaTime); if (sortTimer > 0.0f) { sortTimer -= deltaTime; @@ -130,6 +117,8 @@ namespace Barotrauma objectiveManager.SortObjectives(); sortTimer = sortObjectiveInterval; } + objectiveManager.UpdateObjectives(deltaTime); + if (reactTimer > 0.0f) { reactTimer -= deltaTime; @@ -221,96 +210,316 @@ namespace Barotrauma float speedMultiplier = Character.SpeedMultiplier; if (run || speedMultiplier <= 0.0f) targetMovement *= speedMultiplier; Character.ResetSpeedMultiplier(); // Reset, items will set the value before the next update + + if (Character.AnimController.InWater && targetMovement.LengthSquared() < 0.000001f) + { + bool isAiming = false; + var holdable = Character.SelectedConstruction?.GetComponent(); + if (holdable != null) + { + isAiming = holdable.ControlPose; + } + bool swimInPlace = !isAiming; + if (swimInPlace && ObjectiveManager.GetActiveObjective() is AIObjectiveGoTo goToObjective) + { + if (goToObjective.Target != Character) + { + swimInPlace = false; + } + } + if (swimInPlace) + { + // Swim in place so that we don't fall motionless and look dead. + targetMovement = new Vector2(targetMovement.X, Rand.Range(-0.001f, 0.001f)); + } + } + Character.AnimController.TargetMovement = targetMovement; if (!Character.LockHands) { - DropUnnecessaryItems(); + UnequipUnnecessaryItems(); } - if (Character.IsKeyDown(InputType.Aim)) + flipTimer -= deltaTime; + if (flipTimer <= 0.0f) { - var cursorDiffX = Character.CursorPosition.X - Character.Position.X; - if (cursorDiffX > 10.0f) + Direction newDir = Character.AnimController.TargetDir; + if (Character.IsKeyDown(InputType.Aim)) { - Character.AnimController.TargetDir = Direction.Right; + var cursorDiffX = Character.CursorPosition.X - Character.Position.X; + if (cursorDiffX > 10.0f) + { + newDir = Direction.Right; + } + else if (cursorDiffX < -10.0f) + { + newDir = Direction.Left; + } + if (Character.SelectedConstruction != null) Character.SelectedConstruction.SecondaryUse(deltaTime, Character); } - else if (cursorDiffX < -10.0f) + else if (Math.Abs(Character.AnimController.TargetMovement.X) > 0.1f && !Character.AnimController.InWater) { - Character.AnimController.TargetDir = Direction.Left; + newDir = Character.AnimController.TargetMovement.X > 0.0f ? Direction.Right : Direction.Left; + } + if (newDir != Character.AnimController.TargetDir) + { + Character.AnimController.TargetDir = newDir; + flipTimer = FlipInterval; } - - if (Character.SelectedConstruction != null) Character.SelectedConstruction.SecondaryUse(deltaTime, Character); - - } - else if (Math.Abs(Character.AnimController.TargetMovement.X) > 0.1f && !Character.AnimController.InWater) - { - Character.AnimController.TargetDir = Character.AnimController.TargetMovement.X > 0.0f ? Direction.Right : Direction.Left; } } - private void DropUnnecessaryItems() + private void UnequipUnnecessaryItems() { - if (!NeedsDivingGear(Character.CurrentHull)) + if (ObjectiveManager.HasActiveObjective()) { return; } + if (findItemState == FindItemState.None || findItemState == FindItemState.Extinguisher) { - bool oxygenLow = Character.OxygenAvailable < CharacterHealth.LowOxygenThreshold; - bool highPressure = Character.CurrentHull == null || Character.CurrentHull.LethalPressure > 0 && Character.PressureProtection <= 0; - bool shouldKeepTheGearOn = !ObjectiveManager.IsCurrentObjective(); - bool removeDivingSuit = oxygenLow && !highPressure; - if (!removeDivingSuit) + if (!ObjectiveManager.IsCurrentObjective() && !objectiveManager.HasActiveObjective()) { - bool targetHasNoSuit = objectiveManager.CurrentOrder is AIObjectiveGoTo gtObj && gtObj.mimic && !HasDivingSuit(gtObj.Target as Character); - bool canDropTheSuit = Character.CurrentHull.WaterPercentage < 1 && !Character.IsClimbing && steeringManager == insideSteering && !PathSteering.InStairs; - removeDivingSuit = (!shouldKeepTheGearOn || targetHasNoSuit) && canDropTheSuit; - } - if (removeDivingSuit) - { - var divingSuit = Character.Inventory.FindItemByIdentifier("divingsuit") ?? Character.Inventory.FindItemByTag("divingsuit"); - if (divingSuit != null) + var extinguisher = Character.Inventory.FindItemByTag("extinguisher"); + if (extinguisher != null && Character.HasEquippedItem(extinguisher)) { - // TODO: take the item where it was taken from? - divingSuit.Drop(Character); - } - } - bool targetHasNoMask = objectiveManager.CurrentOrder is AIObjectiveGoTo gotoObjective && gotoObjective.mimic && !HasDivingMask(gotoObjective.Target as Character); - bool takeMaskOff = oxygenLow || (!shouldKeepTheGearOn && Character.CurrentHull.WaterPercentage < 20) || targetHasNoMask; - if (takeMaskOff) - { - var mask = Character.Inventory.FindItemByIdentifier("divingmask"); - if (mask != null && Character.Inventory.IsInLimbSlot(mask, InvSlotType.Head)) - { - // Try to put the mask in an Any slot, and drop it if that fails - if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List() { InvSlotType.Any })) + if (ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) { - mask.Drop(Character); + extinguisher.Drop(Character); + } + else + { + findItemState = FindItemState.Extinguisher; + if (FindSuitableContainer(Character, extinguisher, out Item targetContainer)) + { + findItemState = FindItemState.None; + itemIndex = 0; + if (targetContainer != null) + { + var decontainObjective = new AIObjectiveDecontainItem(Character, extinguisher, targetContainer.GetComponent(), ObjectiveManager, targetContainer.GetComponent()); + decontainObjective.Abandoned += () => ignoredContainers.Add(targetContainer); + ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true); + return; + } + else + { + extinguisher.Drop(Character); + } + } } } } } - if (!ObjectiveManager.IsCurrentObjective() && !ObjectiveManager.IsCurrentObjective()) + if (findItemState == FindItemState.None || findItemState == FindItemState.DivingSuit || findItemState == FindItemState.DivingMask) { - var extinguisherItem = Character.Inventory.FindItemByIdentifier("extinguisher") ?? Character.Inventory.FindItemByTag("extinguisher"); - if (extinguisherItem != null && Character.HasEquippedItem(extinguisherItem)) + if (!NeedsDivingGear(Character, Character.CurrentHull, out _)) { - // TODO: take the item where it was taken from? - extinguisherItem.Drop(Character); - } - } - foreach (var item in Character.Inventory.Items) - { - if (item == null) { continue; } - if (ObjectiveManager.CurrentObjective is AIObjectiveIdle) - { - if (item.AllowedSlots.Contains(InvSlotType.RightHand | InvSlotType.LeftHand) && Character.HasEquippedItem(item)) + bool oxygenLow = Character.OxygenAvailable < CharacterHealth.LowOxygenThreshold; + bool shouldKeepTheGearOn = Character.AnimController.HeadInWater + || Character.CurrentHull.WaterPercentage > 50 + || ObjectiveManager.IsCurrentObjective() + || ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn); + bool removeDivingSuit = !Character.AnimController.HeadInWater && oxygenLow; + AIObjectiveGoTo gotoObjective = ObjectiveManager.CurrentOrder as AIObjectiveGoTo; + if (!removeDivingSuit) { - // Try to put the weapon in an Any slot, and drop it if that fails - if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List() { InvSlotType.Any })) + bool targetHasNoSuit = gotoObjective != null && gotoObjective.mimic && !HasDivingSuit(gotoObjective.Target as Character); + removeDivingSuit = !shouldKeepTheGearOn && (gotoObjective == null || targetHasNoSuit); + } + bool takeMaskOff = !Character.AnimController.HeadInWater && oxygenLow; + if (!takeMaskOff && Character.CurrentHull.WaterPercentage < 40) + { + bool targetHasNoMask = gotoObjective != null && gotoObjective.mimic && !HasDivingMask(gotoObjective.Target as Character); + takeMaskOff = !shouldKeepTheGearOn && (gotoObjective == null || targetHasNoMask); + } + if (gotoObjective != null) + { + if (gotoObjective.Target is Hull h) { - item.Drop(Character); + if (NeedsDivingGear(Character, h, out _)) + { + removeDivingSuit = false; + takeMaskOff = false; + } + } + else if (gotoObjective.Target is Character c) + { + if (NeedsDivingGear(Character, c.CurrentHull, out _)) + { + removeDivingSuit = false; + takeMaskOff = false; + } + } + else if (gotoObjective.Target is Item i) + { + if (NeedsDivingGear(Character, i.CurrentHull, out _)) + { + removeDivingSuit = false; + takeMaskOff = false; + } + } + } + if (findItemState == FindItemState.None || findItemState == FindItemState.DivingSuit) + { + if (removeDivingSuit) + { + var divingSuit = Character.Inventory.FindItemByTag("divingsuit"); + if (divingSuit != null) + { + if (oxygenLow || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) + { + divingSuit.Drop(Character); + } + else + { + findItemState = FindItemState.DivingSuit; + if (FindSuitableContainer(Character, divingSuit, out Item targetContainer)) + { + findItemState = FindItemState.None; + itemIndex = 0; + if (targetContainer != null) + { + var decontainObjective = new AIObjectiveDecontainItem(Character, divingSuit, targetContainer.GetComponent(), ObjectiveManager, targetContainer.GetComponent()); + decontainObjective.Abandoned += () => ignoredContainers.Add(targetContainer); + ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true); + return; + } + else + { + divingSuit.Drop(Character); + } + } + } + } + } + } + if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask) + { + if (takeMaskOff) + { + var mask = Character.Inventory.FindItemByTag("divingmask"); + if (mask != null && Character.Inventory.IsInLimbSlot(mask, InvSlotType.Head)) + { + if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List() { InvSlotType.Any })) + { + if (oxygenLow || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority) + { + mask.Drop(Character); + } + else + { + findItemState = FindItemState.DivingMask; + if (FindSuitableContainer(Character, mask, out Item targetContainer)) + { + findItemState = FindItemState.None; + itemIndex = 0; + if (targetContainer != null) + { + var decontainObjective = new AIObjectiveDecontainItem(Character, mask, targetContainer.GetComponent(), ObjectiveManager, targetContainer.GetComponent()); + decontainObjective.Abandoned += () => ignoredContainers.Add(targetContainer); + ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true); + return; + } + else + { + mask.Drop(Character); + } + } + } + } + } } } } } + if (findItemState == FindItemState.None || findItemState == FindItemState.OtherItem) + { + if (ObjectiveManager.IsCurrentObjective() || + ObjectiveManager.IsCurrentObjective() || + ObjectiveManager.IsCurrentObjective() || + ObjectiveManager.IsCurrentObjective()) + { + foreach (var item in Character.Inventory.Items) + { + if (item == null) { continue; } + if (Character.HasEquippedItem(item) && + (Character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand) || + Character.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand) || + Character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand | InvSlotType.LeftHand))) + { + if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List() { InvSlotType.Any })) + { + if (FindSuitableContainer(Character, item, out Item targetContainer)) + { + findItemState = FindItemState.None; + itemIndex = 0; + if (targetContainer != null) + { + var decontainObjective = new AIObjectiveDecontainItem(Character, item, targetContainer.GetComponent(), ObjectiveManager, targetContainer.GetComponent()); + decontainObjective.Abandoned += () => ignoredContainers.Add(targetContainer); + ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true); + return; + } + else + { + item.Drop(Character); + } + } + else + { + findItemState = FindItemState.OtherItem; + } + } + } + } + } + } + } + + private enum FindItemState + { + None, + DivingSuit, + DivingMask, + Extinguisher, + OtherItem + } + private FindItemState findItemState; + private int itemIndex; + private List ignoredContainers = new List(); + public bool FindSuitableContainer(Character character, Item containableItem, out Item suitableContainer) + { + suitableContainer = null; + if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: ignoredContainers, customPriorityFunction: i => + { + var container = i.GetComponent(); + if (container == null) { return 0; } + if (container.Inventory.IsFull()) { return 0; } + if (container.ShouldBeContained(containableItem, out bool isRestrictionsDefined)) + { + if (isRestrictionsDefined) + { + return 3; + } + else + { + if (containableItem.Prefab.IsContainerPreferred(container, out bool isPreferencesDefined)) + { + return isPreferencesDefined ? 2 : 1; + } + else + { + return isPreferencesDefined ? 0 : 1; + } + } + } + else + { + return 0; + } + })) + { + suitableContainer = targetContainer; + return true; + } + return false; } protected void ReportProblems() @@ -322,7 +531,7 @@ namespace Barotrauma { foreach (Character c in Character.CharacterList) { - if (c.CurrentHull != hull) { continue; } + if (c.CurrentHull != hull || !c.Enabled) { continue; } if (AIObjectiveFightIntruders.IsValidTarget(c, Character)) { AddTargets(Character, c); @@ -559,23 +768,42 @@ namespace Barotrauma shouldCrouch = Submarine.PickBody(startPos, startPos + Vector2.UnitY * minCeilingDist, null, Physics.CollisionWall) != null; } - public static bool NeedsDivingGear(Hull hull) => hull == null || hull.OxygenPercentage < 50 || hull.WaterPercentage > 50; + public static bool NeedsDivingGear(Character character, Hull hull, out bool needsSuit) + { + needsSuit = false; + if (hull == null || + hull.WaterPercentage > 80 || + (hull.LethalPressure > 0 && character.PressureProtection <= 0) || + (hull.ConnectedGaps.Any() && hull.ConnectedGaps.Max(g => AIObjectiveFixLeaks.GetLeakSeverity(g)) > 60)) + { + needsSuit = true; + return true; + } + if (hull.WaterPercentage > 60 || hull.OxygenPercentage < CharacterHealth.LowOxygenThreshold) + { + return true; + } + return false; + } + + + public static bool HasDivingGear(Character character, float conditionPercentage = 0) => HasDivingSuit(character, conditionPercentage) || HasDivingMask(character, conditionPercentage); /// /// Check whether the character has a diving suit in usable condition plus some oxygen. /// - public static bool HasDivingSuit(Character character) => HasItem(character, "divingsuit", "oxygensource"); + public static bool HasDivingSuit(Character character, float conditionPercentage = 0) => HasItem(character, "divingsuit", "oxygensource", conditionPercentage); /// /// Check whether the character has a diving mask in usable condition plus some oxygen. /// - public static bool HasDivingMask(Character character) => HasItem(character, "diving", "oxygensource"); + public static bool HasDivingMask(Character character, float conditionPercentage = 0) => HasItem(character, "divingmask", "oxygensource", conditionPercentage); - public static bool HasItem(Character character, string tag, string containedTag, float conditionPercentage = 0) + public static bool HasItem(Character character, string identifier, string containedTag, float conditionPercentage = 0) { if (character == null) { return false; } if (character.Inventory == null) { return false; } - var item = character.Inventory.FindItemByTag(tag); + var item = character.Inventory.FindItemByIdentifier(identifier) ?? character.Inventory.FindItemByTag(identifier); return item != null && item.ConditionPercentage > conditionPercentage && character.HasEquippedItem(item) && @@ -700,30 +928,27 @@ namespace Barotrauma public float GetHullSafety(Hull hull, Character character, IEnumerable visibleHulls = null) { - bool updateCurrentHullSafety = character == Character && character.CurrentHull == hull; + bool isCurrentHull = character == Character && character.CurrentHull == hull; if (hull == null) { - if (updateCurrentHullSafety) + if (isCurrentHull) { CurrentHullSafety = 0; } return CurrentHullSafety; } - if (character == Character) + if (isCurrentHull && visibleHulls == null) { - // If the character is this character, we can use the cached hulls. - // If no visible hulls are provided, the calculations don't take visible/adjacent hulls into account. - if (visibleHulls == null) - { - visibleHulls = VisibleHulls; - } + // Use the cached visible hulls + visibleHulls = VisibleHulls; } - bool ignoreFire = ObjectiveManager.IsCurrentObjective() || ObjectiveManager.IsCurrentObjective(); + // TODO: should we calculate the visible hulls for each hull? -> could be a bit heavy. + bool ignoreFire = ObjectiveManager.IsCurrentObjective() || objectiveManager.HasActiveObjective(); bool ignoreWater = HasDivingSuit(character); bool ignoreOxygen = ignoreWater || HasDivingMask(character); bool ignoreEnemies = ObjectiveManager.IsCurrentObjective(); float safety = GetHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies); - if (updateCurrentHullSafety) + if (isCurrentHull) { CurrentHullSafety = safety; } @@ -752,7 +977,7 @@ namespace Barotrauma float enemyFactor = 1; if (!ignoreEnemies) { - Func isValidTarget = e => !e.IsDead && !e.IsUnconscious && !e.Removed && !IsFriendly(character, e); + Func isValidTarget = e => IsActive(e) && !IsFriendly(character, e); int enemyCount = visibleHulls == null ? Character.CharacterList.Count(e => e.CurrentHull == hull && isValidTarget(e)) : Character.CharacterList.Count(e => visibleHulls.Contains(e.CurrentHull) && isValidTarget(e)); @@ -763,11 +988,15 @@ namespace Barotrauma return MathHelper.Clamp(safety * 100, 0, 100); } + public void FaceTarget(ISpatialEntity target) => Character.AnimController.TargetDir = target.WorldPosition.X > Character.WorldPosition.X ? Direction.Right : Direction.Left; + public bool IsFriendly(Character other) => IsFriendly(Character, other); public static bool IsFriendly(Character me, Character other) => (other.TeamID == me.TeamID || other.TeamID == Character.TeamType.FriendlyNPC || me.TeamID == Character.TeamType.FriendlyNPC) && (other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group)); + + public static bool IsActive(Character other) => !other.Removed && !other.IsDead && !other.IsUnconscious; } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/IndoorsSteeringManager.cs index 90ade2677..e7aead8d7 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/IndoorsSteeringManager.cs @@ -3,6 +3,7 @@ using Microsoft.Xna.Framework; using System; using System.Linq; using Barotrauma.Extensions; +using FarseerPhysics; namespace Barotrauma { @@ -142,65 +143,60 @@ namespace Barotrauma IsPathDirty = false; } - public Func startNodeFilter; - public Func endNodeFilter; - - protected override Vector2 DoSteeringSeek(Vector2 target, float weight) + public void SteeringSeek(Vector2 target, float weight, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null) { - bool needsNewPath = currentPath != null && currentPath.Unreachable || Vector2.DistanceSquared(target, currentTarget) > 1; + steering += CalculateSteeringSeek(target, weight, startNodeFilter, endNodeFilter, nodeFilter); + } + + private Vector2 CalculateSteeringSeek(Vector2 target, float weight, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null) + { + bool needsNewPath = currentPath == null || (currentPath.Unreachable || currentPath.NextNode == null) || Vector2.DistanceSquared(target, currentTarget) > 1; //find a new path if one hasn't been found yet or the target is different from the current target - if (currentPath == null || needsNewPath || findPathTimer < -1.0f) + if (needsNewPath || findPathTimer < -1.0f) { IsPathDirty = true; - - if (findPathTimer > 0.0f) return Vector2.Zero; - + if (findPathTimer > 0.0f) { return Vector2.Zero; } currentTarget = target; - Vector2 pos = host.SimPosition; - // TODO: remove this and handle differently? + Vector2 currentPos = host.SimPosition; if (character != null && character.Submarine == null) { var targetHull = Hull.FindHull(FarseerPhysics.ConvertUnits.ToDisplayUnits(target), null, false); if (targetHull != null && targetHull.Submarine != null) { - pos -= targetHull.Submarine.SimPosition; + currentPos -= targetHull.Submarine.SimPosition; } } - - var newPath = pathFinder.FindPath(pos, target, character.Submarine, "(Character: " + character.Name + ")", startNodeFilter, endNodeFilter); - bool useNewPath = currentPath == null || needsNewPath; + var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", startNodeFilter, endNodeFilter, nodeFilter); + bool useNewPath = currentPath == null || needsNewPath || currentPath.Finished; if (!useNewPath && currentPath != null && currentPath.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable) { // It's possible that the current path was calculated from a start point that is no longer valid. // Therefore, let's accept also paths with a greater cost than the current, if the current node is much farther than the new start node. - useNewPath = newPath.Cost < currentPath.Cost || - Vector2.DistanceSquared(character.WorldPosition, currentPath.CurrentNode.WorldPosition) > Math.Pow(Vector2.Distance(character.WorldPosition, newPath.Nodes.First().WorldPosition) * 2, 2); + useNewPath = newPath.Cost < currentPath.Cost || + Vector2.DistanceSquared(character.WorldPosition, currentPath.CurrentNode.WorldPosition) > Math.Pow(Vector2.Distance(character.WorldPosition, newPath.Nodes.First().WorldPosition) * 3, 2); } if (useNewPath) { currentPath = newPath; } - findPathTimer = Rand.Range(1.0f, 1.2f); - IsPathDirty = false; - return DiffToCurrentNode(); + return DiffToCurrentNode(); } Vector2 diff = DiffToCurrentNode(); - var collider = character.AnimController.Collider; //if not in water and the waypoint is between the top and bottom of the collider, no need to move vertically if (!character.AnimController.InWater && !character.IsClimbing && diff.Y < collider.height / 2 + collider.radius) { diff.Y = 0.0f; } - - if (diff.LengthSquared() < 0.001f) return -host.Steering; - - return Vector2.Normalize(diff) * weight; + if (diff.LengthSquared() < 0.001f) { return -host.Steering; } + return Vector2.Normalize(diff) * weight; } + protected override Vector2 DoSteeringSeek(Vector2 target, float weight) => CalculateSteeringSeek(target, weight, null, null, null); + private Vector2 DiffToCurrentNode() { if (currentPath == null || currentPath.Unreachable) return Vector2.Zero; @@ -270,13 +266,15 @@ namespace Barotrauma { diff.Y = Math.Max(diff.Y, 1.0f); } - - bool aboveFloor = heightFromFloor > 0 && heightFromFloor < collider.height * 1.5f; + // We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative. + float margin = 0.1f; + bool aboveFloor = heightFromFloor > -margin && heightFromFloor < collider.height * 1.5f; if (aboveFloor || IsNextNodeLadder) { - if (!nextLadderSameAsCurrent) + if (!nextLadderSameAsCurrent || currentPath.NextNode == null && aboveFloor) { character.AnimController.Anim = AnimController.Animation.None; + character.SelectedConstruction = null; } currentPath.SkipToNextNode(); } @@ -302,7 +300,8 @@ namespace Barotrauma character.SelectedConstruction = null; } float multiplier = MathHelper.Lerp(1, 10, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1)); - if (Vector2.DistanceSquared(pos, currentPath.CurrentNode.SimPosition) < MathUtils.Pow(collider.radius * 2 * multiplier, 2)) + float targetDistance = collider.GetSize().X * multiplier; + if (Vector2.DistanceSquared(pos, currentPath.CurrentNode.SimPosition) < MathUtils.Pow(targetDistance, 2)) { currentPath.SkipToNextNode(); } @@ -387,13 +386,13 @@ namespace Barotrauma door = currentWaypoint.ConnectedGap.ConnectedDoor; if (door.LinkedGap.IsHorizontal) { - int currentDir = Math.Sign(nextWaypoint.WorldPosition.X - door.Item.WorldPosition.X); - shouldBeOpen = (door.Item.WorldPosition.X - character.WorldPosition.X) * currentDir > -50.0f; + int dir = Math.Sign(nextWaypoint.WorldPosition.X - door.Item.WorldPosition.X); + shouldBeOpen = (door.Item.WorldPosition.X - character.WorldPosition.X) * dir > -50.0f; } else { - int currentDir = Math.Sign(nextWaypoint.WorldPosition.Y - door.Item.WorldPosition.Y); - shouldBeOpen = (door.Item.WorldPosition.Y - character.WorldPosition.Y) * currentDir > -80.0f; + int dir = Math.Sign(nextWaypoint.WorldPosition.Y - door.Item.WorldPosition.Y); + shouldBeOpen = (door.Item.WorldPosition.Y - character.WorldPosition.Y) * dir > -80.0f; } } } @@ -408,7 +407,18 @@ namespace Barotrauma bool canAccess = CanAccessDoor(door, button => { if (currentWaypoint == null) { return true; } - float distance = Vector2.DistanceSquared(button.Item.WorldPosition, door.Item.WorldPosition); + // Check that the button is on the right side of the door. + if (door.LinkedGap.IsHorizontal) + { + int dir = Math.Sign(nextWaypoint.WorldPosition.X - door.Item.WorldPosition.X); + if (button.Item.WorldPosition.X * dir > door.Item.WorldPosition.X * dir) { return false; } + } + else + { + int dir = Math.Sign(nextWaypoint.WorldPosition.Y - door.Item.WorldPosition.Y); + if (button.Item.WorldPosition.Y * dir > door.Item.WorldPosition.Y * dir) { return false; } + } + float distance = Vector2.DistanceSquared(button.Item.WorldPosition, character.WorldPosition); if (closestButton == null || distance < closestDist) { closestButton = button; @@ -434,17 +444,30 @@ namespace Barotrauma } else { - // Can't reach the button closest to the door. + // Can't reach the button closest to the character. // It's possible that we could reach another buttons. // If this becomes an issue, we could go through them here and check if any of them are reachable // (would have to cache a collection of buttons instead of a single reference in the CanAccess filter method above) - //currentPath.Unreachable = true; + var body = Submarine.PickBody(character.SimPosition, character.GetRelativeSimPosition(closestButton.Item), collisionCategory: Physics.CollisionWall | Physics.CollisionLevel); + if (body != null) + { + if (body.UserData is Item item) + { + var d = item.GetComponent(); + if (d == null || d.IsOpen) { return; } + } + // The button is on the wrong side of the door or a wall + currentPath.Unreachable = true; + } return; } } } else if (shouldBeOpen) { +#if DEBUG + DebugConsole.NewMessage($"{character.Name}: Pathfinding error: Cannot access the door", Color.Yellow); +#endif currentPath.Unreachable = true; return; } @@ -520,6 +543,72 @@ namespace Barotrauma return penalty; } - } - + + public void Wander(float deltaTime, float wallAvoidDistance = 150, bool stayStillInTightSpace = true) + { + //steer away from edges of the hull + bool wander = false; + bool inWater = character.AnimController.InWater; + var currentHull = character.CurrentHull; + if (currentHull != null && !inWater) + { + float roomWidth = currentHull.Rect.Width; + if (stayStillInTightSpace && roomWidth < wallAvoidDistance * 4) + { + Reset(); + } + else + { + float leftDist = character.Position.X - currentHull.Rect.X; + float rightDist = currentHull.Rect.Right - character.Position.X; + if (leftDist < wallAvoidDistance && rightDist < wallAvoidDistance) + { + if (Math.Abs(rightDist - leftDist) > wallAvoidDistance / 2) + { + SteeringManual(deltaTime, Vector2.UnitX * Math.Sign(rightDist - leftDist)); + return; + } + else if (stayStillInTightSpace) + { + Reset(); + return; + } + } + if (leftDist < wallAvoidDistance) + { + float speed = (wallAvoidDistance - leftDist) / wallAvoidDistance; + SteeringManual(deltaTime, Vector2.UnitX * MathHelper.Clamp(speed, 0.25f, 1)); + WanderAngle = 0.0f; + } + else if (rightDist < wallAvoidDistance) + { + float speed = (wallAvoidDistance - rightDist) / wallAvoidDistance; + SteeringManual(deltaTime, -Vector2.UnitX * MathHelper.Clamp(speed, 0.25f, 1)); + WanderAngle = MathHelper.Pi; + } + else + { + wander = true; + } + } + } + else + { + wander = true; + } + if (wander) + { + SteeringWander(); + if (currentHull == null) + { + SteeringAvoid(deltaTime, lookAheadDistance: ConvertUnits.ToSimUnits(wallAvoidDistance)); + } + } + if (!inWater) + { + //reset vertical steering to prevent dropping down from platforms etc + ResetY(); + } + } + } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/LatchOntoAI.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/LatchOntoAI.cs index 5ddf4e7f3..816179149 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/LatchOntoAI.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/LatchOntoAI.cs @@ -133,32 +133,41 @@ namespace Barotrauma case AIState.Idle: if (attachToWalls && character.Submarine == null && Level.Loaded != null) { - raycastTimer -= deltaTime; - //check if there are any walls nearby the character could attach to - if (raycastTimer < 0.0f) + if (!IsAttached) { - wallAttachPos = Vector2.Zero; - - var cells = Level.Loaded.GetCells(character.WorldPosition, 1); - if (cells.Count > 0) + raycastTimer -= deltaTime; + //check if there are any walls nearby the character could attach to + if (raycastTimer < 0.0f) { - foreach (Voronoi2.VoronoiCell cell in cells) + wallAttachPos = Vector2.Zero; + + var cells = Level.Loaded.GetCells(character.WorldPosition, 1); + if (cells.Count > 0) { - foreach (Voronoi2.GraphEdge edge in cell.Edges) + float closestDist = float.PositiveInfinity; + foreach (Voronoi2.VoronoiCell cell in cells) { - if (MathUtils.GetLineIntersection(edge.Point1, edge.Point2, character.WorldPosition, cell.Center, out Vector2 intersection)) + foreach (Voronoi2.GraphEdge edge in cell.Edges) { - attachSurfaceNormal = edge.GetNormal(cell); - attachTargetBody = cell.Body; - wallAttachPos = ConvertUnits.ToSimUnits(intersection); - break; + if (MathUtils.GetLineIntersection(edge.Point1, edge.Point2, character.WorldPosition, cell.Center, out Vector2 intersection)) + { + attachSurfaceNormal = edge.GetNormal(cell); + attachTargetBody = cell.Body; + Vector2 potentialAttachPos = ConvertUnits.ToSimUnits(intersection); + float distSqr = Vector2.DistanceSquared(character.SimPosition, wallAttachPos); + if (distSqr < closestDist) + { + wallAttachPos = potentialAttachPos; + closestDist = distSqr; + } + break; + } } } - if (WallAttachPos != Vector2.Zero) break; } + raycastTimer = RaycastInterval; } - raycastTimer = RaycastInterval; - } + } } else { diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjective.cs index f51799a7a..0eba82daf 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjective.cs @@ -12,6 +12,16 @@ namespace Barotrauma public abstract string DebugTag { get; } public virtual bool ForceRun => false; + public virtual bool IgnoreUnsafeHulls => false; + public virtual bool AbandonWhenCannotCompleteSubjectives => true; + public virtual bool AllowSubObjectiveSorting => false; + public virtual bool ReportFailures => true; + + /// + /// Can there be multiple objective instaces of the same type? Currently multiple instances allowed only for main objectives and the subobjectives of objetive loops. + /// In theory, there could be multiple subobjectives of same type for concurrent objectives, but that would make things more complex -> potential issues + /// + public virtual bool AllowMultipleInstances => false; /// /// Run the main objective with all subobjectives concurrently? @@ -19,19 +29,30 @@ namespace Barotrauma /// public virtual bool ConcurrentObjectives => false; + public virtual bool KeepDivingGearOn => false; + protected readonly List subObjectives = new List(); public float Priority { get; set; } public float PriorityModifier { get; private set; } = 1; public readonly Character character; public readonly AIObjectiveManager objectiveManager; - public string Option { get; protected set; } + public string Option { get; private set; } - protected bool abandon; + private bool _abandon; + public bool Abandon + { + get { return _abandon; } + set + { + _abandon = value; + if (_abandon) + { + OnAbandon(); + } + } + } - /// - /// Can the objective be completed. That is, does the objective have failing subobjectives or other conditions that prevent it from completing. - /// - public virtual bool CanBeCompleted => !abandon && subObjectives.All(so => so.CanBeCompleted); + public virtual bool CanBeCompleted => !Abandon; /// /// When true, the objective is never completed, unless CanBeCompleted returns false. @@ -39,71 +60,85 @@ namespace Barotrauma public virtual bool IsLoop { get; set; } public IEnumerable SubObjectives => subObjectives; + private readonly List all = new List(); + public IEnumerable GetSubObjectivesRecursive(bool includingSelf = false) + { + all.Clear(); + if (includingSelf) + { + all.Add(this); + } + foreach (var subObjective in subObjectives) + { + all.AddRange(subObjective.GetSubObjectivesRecursive(true)); + } + return all; + } + public event Action Completed; + public event Action Abandoned; + public event Action Selected; + public event Action Deselected; protected HumanAIController HumanAIController => character.AIController as HumanAIController; protected IndoorsSteeringManager PathSteering => HumanAIController.PathSteering; protected SteeringManager SteeringManager => HumanAIController.SteeringManager; - + + public AIObjective GetActiveObjective() + { + var subObjective = SubObjectives.FirstOrDefault(); + return subObjective == null ? this : subObjective.GetActiveObjective(); + } + public AIObjective(Character character, AIObjectiveManager objectiveManager, float priorityModifier, string option = null) { this.objectiveManager = objectiveManager; this.character = character; Option = option ?? string.Empty; - PriorityModifier = priorityModifier; -#if DEBUG - IsDuplicate(null); -#endif } /// - /// makes the character act according to the objective, or according to any subobjectives that - /// need to be completed before this one + /// Makes the character act according to the objective, or according to any subobjectives that need to be completed before this one /// public void TryComplete(float deltaTime) { + if (isCompleted) { return; } + //if (Abandon && !IsLoop && subObjectives.None()) { return; } + if (CheckState()) { return; } + // Not ready -> act (can't do foreach because it's possible that the collection is modified in event callbacks. for (int i = 0; i < subObjectives.Count; i++) { - var subObjective = subObjectives[i]; - if (subObjective.IsCompleted()) - { -#if DEBUG - DebugConsole.NewMessage($"Removing subobjective {subObjective.DebugTag} of {DebugTag}, because it is completed."); -#endif - subObjective.OnCompleted(); - subObjectives.Remove(subObjective); - } - else if (!subObjective.CanBeCompleted) - { -#if DEBUG - DebugConsole.NewMessage($"Removing subobjective {subObjective.DebugTag} of {DebugTag}, because it cannot be completed."); -#endif - subObjectives.Remove(subObjective); - } + subObjectives[i].TryComplete(deltaTime); + if (!ConcurrentObjectives) { return; } } - - foreach (AIObjective objective in subObjectives) - { - objective.TryComplete(deltaTime); - if (!ConcurrentObjectives) - { - return; - } - } - Act(deltaTime); - if (IsCompleted()) + } + + // TODO: check turret aioperate + public void AddSubObjective(AIObjective objective, bool addFirst = false) + { + var type = objective.GetType(); + subObjectives.RemoveAll(o => o.GetType() == type); + if (addFirst) { - OnCompleted(); + subObjectives.Insert(0, objective); + } + else + { + subObjectives.Add(objective); } } - // TODO: go through AIOperate methods where subobjectives are added and ensure that they add the subobjectives correctly -> use TryAddSubObjective method instead? - public void AddSubObjective(AIObjective objective) + /// + /// This method allows multiple subobjectives of same type. Use with caution. + /// + public void AddSubObjectiveInQueue(AIObjective objective) { - if (subObjectives.Any(o => o.IsDuplicate(objective))) { return; } - subObjectives.Add(objective); + if (!subObjectives.Contains(objective)) + { + subObjectives.Add(objective); + } } public void RemoveSubObjective(ref T objective) where T : AIObjective @@ -120,6 +155,7 @@ namespace Barotrauma public void SortSubObjectives() { + if (!AllowSubObjectiveSorting) { return; } if (subObjectives.None()) { return; } subObjectives.Sort((x, y) => y.GetPriority().CompareTo(x.GetPriority())); if (ConcurrentObjectives) @@ -134,6 +170,8 @@ namespace Barotrauma public virtual float GetPriority() => Priority * PriorityModifier; + public virtual bool IsDuplicate(T otherObjective) where T : AIObjective => otherObjective.Option == Option; + public virtual void Update(float deltaTime) { if (objectiveManager.CurrentOrder == this) @@ -150,8 +188,8 @@ namespace Barotrauma } } Priority = MathHelper.Clamp(Priority, 0, 100); - subObjectives.ForEach(so => so.Update(deltaTime)); } + subObjectives.ForEach(so => so.Update(deltaTime)); } /// @@ -174,9 +212,9 @@ namespace Barotrauma /// /// Checks if the objective already is created and added in subobjectives. If not, creates it. /// Handles objectives that cannot be completed. If the objective has been removed form the subobjectives, a null value is assigned to the reference. - /// Returns true if the objective was created. + /// Returns true if the objective was created and successfully added. /// - protected bool TryAddSubObjective(ref T objective, Func constructor, Action onAbandon = null) where T : AIObjective + protected bool TryAddSubObjective(ref T objective, Func constructor, Action onCompleted = null, Action onAbandon = null) where T : AIObjective { if (objective != null) { @@ -184,11 +222,6 @@ namespace Barotrauma // If the sub objective is removed -> it's either completed or impossible to complete. if (!subObjectives.Contains(objective)) { - if (!objective.CanBeCompleted) - { - abandon = true; - onAbandon?.Invoke(); - } objective = null; } return false; @@ -198,37 +231,122 @@ namespace Barotrauma objective = constructor(); if (!subObjectives.Contains(objective)) { - AddSubObjective(objective); + if (objective.AllowMultipleInstances) + { + subObjectives.Add(objective); + } + else + { + AddSubObjective(objective); + } + if (onCompleted != null) + { + objective.Completed += onCompleted; + } + if (onAbandon != null) + { + objective.Abandoned += onAbandon; + } + return true; } - return true; +#if DEBUG + DebugConsole.ThrowError("Attempted to add a duplicate subobjective!\n" + Environment.StackTrace); +#endif + return false; } } public virtual void OnSelected() { - // Should we reset steering here? - //if (!ConcurrentObjectives) - //{ - // SteeringManager.Reset(); - //} + Reset(); + Selected?.Invoke(); + } + + public virtual void OnDeselected() + { + Deselected?.Invoke(); } protected virtual void OnCompleted() { Completed?.Invoke(); - //if (Completed != null) - //{ - // Completed(); - // Completed = null; - //} } - public virtual void Reset() { } + protected virtual void OnAbandon() + { + Abandoned?.Invoke(); + } + + public virtual void Reset() + { + isCompleted = false; + hasBeenChecked = false; + _abandon = false; + } protected abstract void Act(float deltaTime); - public abstract bool IsCompleted(); + private bool isCompleted; + private bool hasBeenChecked; - public abstract bool IsDuplicate(AIObjective otherObjective); + public bool IsCompleted + { + get + { + if (!hasBeenChecked) + { + CheckState(); + } + return isCompleted; + } + protected set + { + isCompleted = value; + } + } + + protected abstract bool Check(); + + private bool CheckState() + { + hasBeenChecked = true; + CheckSubObjectives(); + if (subObjectives.None()) + { + if (Check()) + { + isCompleted = true; + OnCompleted(); + } + } + return isCompleted; + } + + private void CheckSubObjectives() + { + for (int i = 0; i < subObjectives.Count; i++) + { + var subObjective = subObjectives[i]; + subObjective.CheckState(); + if (subObjective.IsCompleted) + { +#if DEBUG + DebugConsole.NewMessage($"{character.Name}: Removing SUBobjective {subObjective.DebugTag} of {DebugTag}, because it is completed.", Color.LightGreen); +#endif + subObjectives.Remove(subObjective); + } + else if (!subObjective.CanBeCompleted) + { +#if DEBUG + DebugConsole.NewMessage($"{character.Name}: Removing SUBobjective {subObjective.DebugTag} of {DebugTag}, because it cannot be completed.", Color.Red); +#endif + subObjectives.Remove(subObjective); + if (AbandonWhenCannotCompleteSubjectives) + { + Abandon = true; + } + } + } + } } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs index e96b00e6b..fc083ed4f 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs @@ -15,11 +15,6 @@ namespace Barotrauma public AIObjectiveChargeBatteries(Character character, AIObjectiveManager objectiveManager, string option, float priorityModifier) : base(character, objectiveManager, priorityModifier, option) { } - public override bool IsDuplicate(AIObjective otherObjective) - { - return otherObjective is AIObjectiveChargeBatteries other && other.Option == Option; - } - protected override bool Filter(PowerContainer battery) { if (battery == null) { return false; } @@ -29,15 +24,8 @@ namespace Barotrauma if (item.Submarine.TeamID != character.TeamID) { return false; } if (item.ConditionPercentage <= 0) { return false; } if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { return false; } - if (Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !HumanAIController.IsFriendly(c))) { return false; } - if (Option == "charge") - { - if (battery.RechargeRatio >= PowerContainer.aiRechargeTargetRatio - 0.01f) { return false; } - } - else - { - if (battery.RechargeRatio <= 0) { return false; } - } + if (Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; } + if (IsReady(battery)) { return false; } return true; } @@ -67,8 +55,26 @@ namespace Barotrauma return batteryList; } - protected override AIObjective ObjectiveConstructor(PowerContainer battery) - => new AIObjectiveOperateItem(battery, character, objectiveManager, Option, false, priorityModifier: PriorityModifier) { IsLoop = false }; + private bool IsReady(PowerContainer battery) + { + if (battery.HasBeenTuned && character.CurrentOrder == null) { return true; } + if (Option == "charge") + { + return battery.RechargeRatio >= PowerContainer.aiRechargeTargetRatio; + } + else + { + return battery.RechargeRatio <= 0; + } + } + + protected override AIObjective ObjectiveConstructor(PowerContainer battery) => + new AIObjectiveOperateItem(battery, character, objectiveManager, Option, false, priorityModifier: PriorityModifier) + { + IsLoop = false, + Override = character.CurrentOrder != null, + completionCondition = () => IsReady(battery) + }; protected override void OnObjectiveCompleted(AIObjective objective, PowerContainer target) => HumanAIController.RemoveTargets(character, target); diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveCombat.cs index 94ac4404c..435a06077 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; using Barotrauma.Extensions; -using FarseerPhysics; namespace Barotrauma { @@ -12,9 +11,19 @@ namespace Barotrauma { public override string DebugTag => "combat"; + public override bool KeepDivingGearOn => true; + public override bool IgnoreUnsafeHulls => true; + private readonly CombatMode initialMode; + private float seekWeaponsTimer; + const float seekWeaponsInterval = 1; + private float ignoreWeaponTimer; + const float ignoredWeaponsClearTime = 10; + const float coolDown = 10.0f; + // Won't take the offensive with weapons that have lower priority than this + const float goodWeaponPriority = 30; public Character Enemy { get; private set; } public bool HoldPosition { get; set; } @@ -48,9 +57,11 @@ namespace Barotrauma } public override bool ConcurrentObjectives => true; + public override bool AbandonWhenCannotCompleteSubjectives => false; private readonly AIObjectiveFindSafety findSafety; private readonly HashSet weapons = new HashSet(); + private readonly HashSet ignoredWeapons = new HashSet(); private AIObjectiveContainItem seekAmmunition; private AIObjectiveGoTo retreatObjective; @@ -79,7 +90,7 @@ namespace Barotrauma if (findSafety != null) { findSafety.Priority = 0; - findSafety.unreachable.Clear(); + HumanAIController.UnreachableHulls.Clear(); } Mode = mode; initialMode = Mode; @@ -91,15 +102,19 @@ namespace Barotrauma public override float GetPriority() => (Enemy != null && (Enemy.Removed || Enemy.IsDead)) ? 0 : Math.Min(100 * PriorityModifier, 100); - public override bool IsDuplicate(AIObjective otherObjective) + public override void Update(float deltaTime) { - if (!(otherObjective is AIObjectiveCombat objective)) return false; - return objective.Enemy == Enemy; + base.Update(deltaTime); + ignoreWeaponTimer -= deltaTime; + seekWeaponsTimer -= deltaTime; + if (ignoreWeaponTimer < 0) + { + ignoredWeapons.Clear(); + ignoreWeaponTimer = ignoredWeaponsClearTime; + } } - public override void OnSelected() => Weapon = null; - - public override bool IsCompleted() + protected override bool Check() { bool completed = (Enemy != null && (Enemy.Removed || Enemy.IsDead)) || (initialMode != CombatMode.Offensive && coolDownTimer <= 0); if (completed) @@ -122,18 +137,16 @@ namespace Barotrauma { coolDownTimer -= deltaTime; } - if (abandon) { return; } - TryArm(); - if (seekAmmunition == null || !subObjectives.Contains(seekAmmunition)) + if (seekAmmunition == null) { - if (!HoldPosition) - { - Move(); - } - if (WeaponComponent != null) + if (TryArm()) { OperateWeapon(deltaTime); } + if (!HoldPosition && seekAmmunition == null) + { + Move(); + } } } @@ -153,42 +166,117 @@ namespace Barotrauma } } + private bool IsLoaded(ItemComponent weapon) => weapon.HasRequiredContainedItems(character, addMessage: false); + private bool TryArm() { - if (character.LockHands) { return false; } - - if (Weapon != null) + if (character.LockHands || Enemy == null) { - if (!character.Inventory.Items.Contains(Weapon) || WeaponComponent == null) + Weapon = null; + return false; + } + if (seekWeaponsTimer < 0) + { + seekWeaponsTimer = seekWeaponsInterval; + // First go through all weapons and try to reload without seeking ammunition + var allWeapons = GetAllWeapons().ToList(); + while (allWeapons.Any()) { - Weapon = null; - } - else if (!WeaponComponent.HasRequiredContainedItems(character, addMessage: false)) - { - // Seek ammunition only if cannot find a new weapon - if (!Reload(!HoldPosition, () => GetWeapon(out _) == null)) + Weapon = GetWeapon(allWeapons, out _weaponComponent); + if (Weapon == null) { - if (seekAmmunition != null && subObjectives.Contains(seekAmmunition)) - { - return false; - } - else + // No weapons + break; + } + if (!character.Inventory.Items.Contains(Weapon) || WeaponComponent == null) + { + // Not in the inventory anymore or cannot find the weapon component + allWeapons.Remove(WeaponComponent); + Weapon = null; + continue; + } + if (initialMode == CombatMode.Offensive) + { + // In the offensive mode, let's ignore weapons that cannot be used in the offensive mode + if (WeaponComponent.CombatPriority < goodWeaponPriority) { + allWeapons.Remove(WeaponComponent); Weapon = null; + continue; + } + } + if (IsLoaded(WeaponComponent)) + { + // All good, the weapon is loaded + break; + } + if (Reload(seekAmmo: false)) + { + // All good, reloading successful + break; + } + else + { + // No ammo. + allWeapons.Remove(WeaponComponent); + Weapon = null; + } + } + if (Weapon == null) + { + // No weapon found with the conditions above. Try again, now let's try to seek ammunition too + Weapon = GetWeapon(out _weaponComponent); + if (Weapon != null) + { + if (!CheckWeapon(seekAmmo: true)) + { + if (seekAmmunition != null) + { + // No loaded weapon, but we are trying to seek ammunition. + return false; + } + else + { + Weapon = null; + } } } } } - if (Weapon == null) + else { - Weapon = GetWeapon(out _weaponComponent); + if (!CheckWeapon(seekAmmo: false)) + { + Weapon = null; + } } if (Weapon == null) { - Weapon = GetWeapon(out _weaponComponent, ignoreRequiredItems: true); + Mode = CombatMode.Retreat; + } + else + { + Mode = WeaponComponent.CombatPriority >= goodWeaponPriority ? initialMode : CombatMode.Defensive; } - Mode = Weapon == null ? CombatMode.Retreat : initialMode; return Weapon != null; + + bool CheckWeapon(bool seekAmmo) + { + if (!character.Inventory.Items.Contains(Weapon) || WeaponComponent == null) + { + // Not in the inventory anymore or cannot find the weapon component + return false; + } + if (!IsLoaded(WeaponComponent)) + { + // Try reloading (and seek ammo) + if (!Reload(seekAmmo)) + { + return false; + } + } + return true; + }; } private void OperateWeapon(float deltaTime) @@ -209,59 +297,72 @@ namespace Barotrauma } } - private Item GetWeapon(out ItemComponent weaponComponent, bool ignoreRequiredItems = false) + private Item GetWeapon(out ItemComponent weaponComponent) { - weapons.Clear(); - _weaponComponent = null; - foreach (var item in character.Inventory.Items) - { - if (item == null) { continue; } - SeekWeapons(item); - if (item.OwnInventory != null) - { - item.OwnInventory.Items.ForEach(i => SeekWeapons(i)); - } - } - weaponComponent = weapons.OrderByDescending(w => w.CombatPriority).FirstOrDefault(); + GetAllWeapons(); + return GetWeapon(weapons, out weaponComponent); + } + + private Item GetWeapon(IEnumerable weaponList, out ItemComponent weaponComponent) + { + weaponComponent = weaponList.OrderByDescending(w => CalculateWeaponPriority(w)).FirstOrDefault(); if (weaponComponent == null) { return null; } if (weaponComponent.CombatPriority < 1) { return null; } return weaponComponent.Item; + } - void SeekWeapons(Item item) + private float CalculateWeaponPriority(ItemComponent weapon) + { + float priority = weapon.CombatPriority; + // Halve the priority for weapons that don't have proper ammunition loaded. + if (!weapon.HasRequiredContainedItems(character, addMessage: false)) { - if (item == null) { return; } - foreach (var component in item.Components) + priority /= 2; + } + return priority; + } + + private HashSet GetAllWeapons() + { + weapons.Clear(); + foreach (var item in character.Inventory.Items) + { + if (item == null) { continue; } + if (ignoredWeapons.Contains(item)) { continue; } + SeekWeapons(item, weapons); + if (item.OwnInventory != null) { - if (component is RangedWeapon rw) + item.OwnInventory.Items.ForEach(i => SeekWeapons(i, weapons)); + } + } + return weapons; + } + + private void SeekWeapons(Item item, ICollection weaponList) + { + if (item == null) { return; } + foreach (var component in item.Components) + { + if (component is RangedWeapon rw) + { + weaponList.Add(rw); + } + else if (component is MeleeWeapon mw) + { + weaponList.Add(mw); + } + else + { + var effects = component.statusEffectLists; + if (effects != null) { - if (ignoreRequiredItems || rw.HasRequiredContainedItems(character, addMessage: false)) + foreach (var statusEffects in effects.Values) { - weapons.Add(rw); - } - } - else if (component is MeleeWeapon mw) - { - if (ignoreRequiredItems || mw.HasRequiredContainedItems(character, addMessage: false)) - { - weapons.Add(mw); - } - } - else - { - var effects = component.statusEffectLists; - if (effects != null) - { - foreach (var statusEffects in effects.Values) + foreach (var statusEffect in statusEffects) { - foreach (var statusEffect in statusEffects) + if (statusEffect.Afflictions.Any()) { - if (statusEffect.Afflictions.Any()) - { - if (ignoreRequiredItems || component.HasRequiredContainedItems(character, addMessage: false)) - { - weapons.Add(component); - } - } + weaponList.Add(component); } } } @@ -286,17 +387,14 @@ namespace Barotrauma if (character.LockHands) { return false; } if (!WeaponComponent.HasRequiredContainedItems(character, addMessage: false)) { - Mode = CombatMode.Retreat; return false; } - //if (!character.SelectedItems.Contains(Weapon)) if (!character.HasEquippedItem(Weapon)) { Weapon.TryInteract(character, forceSelectKey: true); var slots = Weapon.AllowedSlots.FindAll(s => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand)); if (character.Inventory.TryPutItem(Weapon, character, slots)) { - Weapon.Equip(character); aimTimer = Rand.Range(0.5f, 1f); } else @@ -323,13 +421,15 @@ namespace Barotrauma } if (character.CurrentHull != retreatTarget) { - TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager, false, true)); + TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager, false, true), + onAbandon: () => Abandon = true, + onCompleted: () => RemoveSubObjective(ref retreatObjective)); } } private void Engage() { - if (character.LockHands) + if (character.LockHands || Enemy == null) { Mode = CombatMode.Retreat; SteeringManager.Reset(); @@ -346,18 +446,18 @@ namespace Barotrauma TryAddSubObjective(ref followTargetObjective, constructor: () => new AIObjectiveGoTo(Enemy, character, objectiveManager, repeat: true, getDivingGearIfNeeded: true) { - AllowGoingOutside = true, IgnoreIfTargetDead = true }, onAbandon: () => { - Mode = CombatMode.Retreat; + Mode = CombatMode.Defensive; SteeringManager.Reset(); + RemoveSubObjective(ref followTargetObjective); }); - if (followTargetObjective != null && subObjectives.Contains(followTargetObjective)) + if (followTargetObjective != null) { followTargetObjective.CloseEnough = - WeaponComponent is RangedWeapon ? 300 : + WeaponComponent is RangedWeapon ? 1000 : WeaponComponent is MeleeWeapon mw ? mw.Range : WeaponComponent is RepairTool rt ? rt.Range : 50; } @@ -377,29 +477,40 @@ namespace Barotrauma targetItemCount = Weapon.GetComponent().Capacity, checkInventory = false }, + onCompleted: () => RemoveSubObjective(ref seekAmmunition), onAbandon: () => { - Weapon = null; - Mode = CombatMode.Retreat; SteeringManager.Reset(); + RemoveSubObjective(ref seekAmmunition); + ignoredWeapons.Add(Weapon); + Weapon = null; }); } /// /// Reloads the ammunition found in the inventory. - /// If seekAmmo is true and the condition is met or not provided, tries to get find the ammo elsewhere. + /// If seekAmmo is true, tries to get find the ammo elsewhere. /// - private bool Reload(bool seekAmmo, Func condition = null) + private bool Reload(bool seekAmmo) { if (WeaponComponent == null) { return false; } if (!WeaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained)) { return false; } var containedItems = Weapon.ContainedItems; + // Drop empty ammo + foreach (Item containedItem in containedItems) + { + if (containedItem == null) { continue; } + if (containedItem.Condition <= 0) + { + containedItem.Drop(character); + } + } RelatedItem item = null; Item ammunition = null; string[] ammunitionIdentifiers = null; foreach (RelatedItem requiredItem in WeaponComponent.requiredItems[RelatedItem.RelationType.Contained]) { - ammunition = containedItems.FirstOrDefault(it => it.Condition > 0.0f && requiredItem.MatchesItem(it)); + ammunition = containedItems.FirstOrDefault(it => it.Condition > 0 && requiredItem.MatchesItem(it)); if (ammunition != null) { // Ammunition still remaining @@ -411,20 +522,25 @@ namespace Barotrauma // No ammo if (ammunition == null) { - var container = Weapon.GetComponent(); - // Try reload ammunition in inventory - foreach (string identifier in ammunitionIdentifiers) + if (ammunitionIdentifiers != null) { - foreach (var i in character.Inventory.Items) + // Try reload ammunition from inventory + ammunition = character.Inventory.FindItem(i => ammunitionIdentifiers.Any(id => id == i.Prefab.Identifier || i.HasTag(id)) && i.Condition > 0, true); + if (ammunition != null) { - if (i == null) { continue; } - if (i.Prefab.Identifier == identifier || i.HasTag(identifier)) + var container = Weapon.GetComponent(); + if (container.Item.ParentInventory == character.Inventory) { - if (i.Condition > 0) + character.Inventory.RemoveItem(ammunition); + if (!container.Inventory.TryPutItem(ammunition, null)) { - container.Inventory.TryPutItem(ammunition, null); + ammunition.Drop(character); } } + else + { + container.Combine(ammunition, character); + } } } } @@ -432,12 +548,9 @@ namespace Barotrauma { return true; } - else if (ammunition == null) + else if (ammunition == null && !HoldPosition && initialMode == CombatMode.Offensive && seekAmmo && ammunitionIdentifiers != null) { - if (seekAmmo && ammunitionIdentifiers != null && (condition == null || condition())) - { - SeekAmmunition(ammunitionIdentifiers); - } + SeekAmmunition(ammunitionIdentifiers); } return false; } @@ -495,7 +608,8 @@ namespace Barotrauma { myBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody); } - var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall; + + var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel; var pickedBody = Submarine.PickBody(Weapon.SimPosition, Enemy.SimPosition, myBodies, collisionCategories); if (pickedBody != null) { @@ -512,7 +626,16 @@ namespace Barotrauma { character.SetInput(InputType.Shoot, false, true); Weapon.Use(deltaTime, character); - aimTimer = Rand.Range(0.25f, 0.5f); + float reloadTime = 0; + if (WeaponComponent is RangedWeapon rangedWeapon) + { + reloadTime = rangedWeapon.Reload; + } + if (WeaponComponent is MeleeWeapon mw) + { + reloadTime = mw.Reload; + } + aimTimer = reloadTime * Rand.Range(1f, 1.5f); } } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveContainItem.cs index f9ee68dff..95d53f3a8 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -19,17 +19,28 @@ namespace Barotrauma //can either be a tag or an identifier public readonly string[] itemIdentifiers; public readonly ItemContainer container; + public readonly Item item; private AIObjectiveGetItem getItemObjective; private AIObjectiveGoTo goToObjective; private readonly HashSet containedItems = new HashSet(); + public bool AllowToFindDivingGear { get; set; } = true; + public float ConditionLevel { get; set; } + + public AIObjectiveContainItem(Character character, Item item, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1) + : base(character, objectiveManager, priorityModifier) + { + this.container = container; + this.item = item; + } + public AIObjectiveContainItem(Character character, string itemIdentifier, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1) : this(character, new string[] { itemIdentifier }, container, objectiveManager, priorityModifier) { } public AIObjectiveContainItem(Character character, string[] itemIdentifiers, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1) - : base (character, objectiveManager, priorityModifier) + : base(character, objectiveManager, priorityModifier) { this.itemIdentifiers = itemIdentifiers; for (int i = 0; i < itemIdentifiers.Length; i++) @@ -40,17 +51,25 @@ namespace Barotrauma this.container = container; } - public override bool IsCompleted() + protected override bool Check() { - int containedItemCount = 0; - foreach (Item item in container.Inventory.Items) + if (IsCompleted) { return true; } + if (item != null) { - if (item != null && itemIdentifiers.Any(id => item.Prefab.Identifier == id || item.HasTag(id))) - { - containedItemCount++; - } + return container.Inventory.Items.Contains(item); + } + else + { + int containedItemCount = 0; + foreach (Item i in container.Inventory.Items) + { + if (i != null && itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id))) + { + containedItemCount++; + } + } + return containedItemCount >= targetItemCount; } - return containedItemCount >= targetItemCount; } public override float GetPriority() @@ -62,20 +81,58 @@ namespace Barotrauma return 1.0f; } + private bool CheckItem(Item i) => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)) && i.ConditionPercentage > ConditionLevel; + protected override void Act(float deltaTime) { - //get the item that should be contained - Item itemToContain = null; - foreach (string identifier in itemIdentifiers) + Item itemToContain = item ?? character.Inventory.FindItem(i => CheckItem(i) && i.Container != container.Item, recursive: true); + if (itemToContain != null) { - itemToContain = character.Inventory.FindItemByIdentifier(identifier) ?? character.Inventory.FindItemByTag(identifier); - if (itemToContain != null && itemToContain.Condition > 0.0f) { break; } - } - if (itemToContain == null) - { - if (getItemObjective != null) + // Contain the item + if (itemToContain.ParentInventory == character.Inventory) { - if (getItemObjective.IsCompleted()) + character.Inventory.RemoveItem(itemToContain); + if (!container.Inventory.TryPutItem(itemToContain, null)) + { + itemToContain.Drop(character); + Abandon = true; + } + } + else + { + if (character.CanInteractWith(container.Item, out _, checkLinked: false)) + { + if (container.Combine(itemToContain, character)) + { + IsCompleted = true; + } + else + { + Abandon = true; + } + } + else + { + TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(container.Item, character, objectiveManager, getDivingGearIfNeeded: AllowToFindDivingGear), + onAbandon: () => Abandon = true, + onCompleted: () => RemoveSubObjective(ref goToObjective)); + } + } + } + else + { + // No matching items in the inventory, try to get an item + TryAddSubObjective(ref getItemObjective, () => + new AIObjectiveGetItem(character, itemIdentifiers, objectiveManager, equip: false, checkInventory: checkInventory) + { + GetItemPriority = GetItemPriority, + ignoredContainerIdentifiers = ignoredContainerIdentifiers, + ignoredItems = containedItems, + AllowToFindDivingGear = this.AllowToFindDivingGear + }, onAbandon: () => + { + Abandon = true; + }, onCompleted: () => { if (getItemObjective.TargetItem != null) { @@ -83,55 +140,18 @@ namespace Barotrauma } else { - // Reduce the target item count to prevent getting stuck here, if the target item for some reason is null, which shouldn't happen. - targetItemCount--; + if (container.Inventory.FindItem(i => CheckItem(i), recursive: false) != null) + { + IsCompleted = true; + } + else + { + Abandon = true; + } } - getItemObjective = null; - } - else if (!getItemObjective.CanBeCompleted) - { - getItemObjective = null; - targetItemCount--; - } - } - TryAddSubObjective(ref getItemObjective, () => - new AIObjectiveGetItem(character, itemIdentifiers, objectiveManager, checkInventory: checkInventory) - { - GetItemPriority = GetItemPriority, - ignoredContainerIdentifiers = ignoredContainerIdentifiers, - ignoredItems = containedItems + RemoveSubObjective(ref getItemObjective); }); - return; } - if (container.Item.ParentInventory == character.Inventory) - { - character.Inventory.RemoveItem(itemToContain); - container.Inventory.TryPutItem(itemToContain, null); - } - else - { - if (!character.CanInteractWith(container.Item, out _, checkLinked: false)) - { - TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(container.Item, character, objectiveManager)); - return; - } - container.Combine(itemToContain, character); - } - } - - public override bool IsDuplicate(AIObjective otherObjective) - { - if (!(otherObjective is AIObjectiveContainItem objective)) { return false; } - if (objective.container != container) { return false; } - if (objective.itemIdentifiers.Length != itemIdentifiers.Length) { return false; } - for (int i = 0; i < itemIdentifiers.Length; i++) - { - if (objective.itemIdentifiers[i] != itemIdentifiers[i]) - { - return false; - } - } - return true; - } + } } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveDecontainItem.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveDecontainItem.cs index c26fbbc51..eda55095b 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveDecontainItem.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveDecontainItem.cs @@ -1,6 +1,7 @@ using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; +using System.Linq; namespace Barotrauma { @@ -12,24 +13,25 @@ namespace Barotrauma //can either be a tag or an identifier private readonly string[] itemIdentifiers; - private readonly ItemContainer container; + private readonly ItemContainer sourceContainer; + private ItemContainer targetContainer; private readonly Item targetItem; private AIObjectiveGoTo goToObjective; - private bool isCompleted; + private AIObjectiveContainItem containObjective; - public AIObjectiveDecontainItem(Character character, Item targetItem, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1) + public AIObjectiveDecontainItem(Character character, Item targetItem, ItemContainer sourceContainer, AIObjectiveManager objectiveManager, ItemContainer targetContainer = null, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { this.targetItem = targetItem; - this.container = container; + this.sourceContainer = sourceContainer; + this.targetContainer = targetContainer; } + public AIObjectiveDecontainItem(Character character, string itemIdentifier, ItemContainer sourceContainer, AIObjectiveManager objectiveManager, ItemContainer targetContainer = null, float priorityModifier = 1) + : this(character, new string[] { itemIdentifier }, sourceContainer, objectiveManager, targetContainer, priorityModifier) { } - public AIObjectiveDecontainItem(Character character, string itemIdentifier, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1) - : this(character, new string[] { itemIdentifier }, container, objectiveManager, priorityModifier) { } - - public AIObjectiveDecontainItem(Character character, string[] itemIdentifiers, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1) + public AIObjectiveDecontainItem(Character character, string[] itemIdentifiers, ItemContainer sourceContainer, AIObjectiveManager objectiveManager, ItemContainer targetContainer = null, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { this.itemIdentifiers = itemIdentifiers; @@ -37,10 +39,11 @@ namespace Barotrauma { itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant(); } - this.container = container; + this.sourceContainer = sourceContainer; + this.targetContainer = targetContainer; } - public override bool IsCompleted() => isCompleted; + protected override bool Check() => IsCompleted; public override float GetPriority() { @@ -53,58 +56,50 @@ namespace Barotrauma protected override void Act(float deltaTime) { - if (isCompleted) { return; } - Item itemToDecontain = null; - //get the item that should be de-contained - if (targetItem == null) + Item itemToDecontain = targetItem ?? sourceContainer.Inventory.FindItem(i => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)), recursive: false); + if (itemToDecontain == null) { - if (itemIdentifiers != null) + Abandon = true; + return; + } + if (targetContainer == null) + { + if (itemToDecontain.Container != sourceContainer.Item) { - foreach (string identifier in itemIdentifiers) - { - itemToDecontain = container.Inventory.FindItemByIdentifier(identifier) ?? container.Inventory.FindItemByTag(identifier); - if (itemToDecontain != null) { break; } - } + IsCompleted = true; + return; } } else { - itemToDecontain = targetItem; - } - if (itemToDecontain == null || itemToDecontain.Container != container.Item) // Item not found or already de-contained, consider complete - { - isCompleted = true; - return; - } - if (itemToDecontain.OwnInventory != character.Inventory && itemToDecontain.ParentInventory != character.Inventory) - { - if (!character.CanInteractWith(container.Item, out _, checkLinked: false)) + if (targetContainer.Inventory.Items.Contains(itemToDecontain)) { - TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(container.Item, character, objectiveManager)); + IsCompleted = true; return; } } - itemToDecontain.Drop(character); - isCompleted = true; - } - - public override bool IsDuplicate(AIObjective otherObjective) - { - if (!(otherObjective is AIObjectiveDecontainItem decontainItem)) { return false; } - if (decontainItem.itemIdentifiers != null && itemIdentifiers != null) + if (goToObjective == null && !itemToDecontain.IsOwnedBy(character)) { - if (decontainItem.itemIdentifiers.Length != itemIdentifiers.Length) { return false; } - for (int i = 0; i < decontainItem.itemIdentifiers.Length; i++) + if (!character.CanInteractWith(sourceContainer.Item, out _, checkLinked: false)) { - if (decontainItem.itemIdentifiers[i] != itemIdentifiers[i]) { return false; } + TryAddSubObjective(ref goToObjective, + constructor: () => new AIObjectiveGoTo(sourceContainer.Item, character, objectiveManager), + onAbandon: () => Abandon = true); + return; } - return true; } - else if (decontainItem.itemIdentifiers == null && itemIdentifiers == null) + if (targetContainer != null) { - return decontainItem.targetItem == targetItem; + TryAddSubObjective(ref containObjective, + constructor: () => new AIObjectiveContainItem(character, itemToDecontain, targetContainer, objectiveManager) { GetItemPriority = this.GetItemPriority }, + onCompleted: () => IsCompleted = true, + onAbandon: () => targetContainer = null); + } + else + { + itemToDecontain.Drop(character); + IsCompleted = true; } - return false; } } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs index 3d29eb047..312e53262 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -12,6 +12,7 @@ namespace Barotrauma public override string DebugTag => "extinguish fire"; public override bool ForceRun => true; public override bool ConcurrentObjectives => true; + public override bool KeepDivingGearOn => true; private readonly Hull targetHull; @@ -27,19 +28,23 @@ namespace Barotrauma public override float GetPriority() { - if (Character.CharacterList.Any(c => c.CurrentHull == targetHull && !HumanAIController.IsFriendly(c))) { return 0; } - // Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally) - float dist = Math.Abs(character.WorldPosition.X - targetHull.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - targetHull.WorldPosition.Y) * 2.0f; - float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 10000, dist)); + if (!objectiveManager.IsCurrentOrder() + && Character.CharacterList.Any(c => c.CurrentHull == targetHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return 0; } + float yDist = Math.Abs(character.WorldPosition.Y - targetHull.WorldPosition.Y); + yDist = yDist > 100 ? yDist * 3 : 0; + float dist = Math.Abs(character.WorldPosition.X - targetHull.WorldPosition.X) + yDist; + float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist)); + if (targetHull == character.CurrentHull) + { + distanceFactor = 1; + } float severity = AIObjectiveExtinguishFires.GetFireSeverity(targetHull); float severityFactor = MathHelper.Lerp(0, 1, severity / 100); float devotion = Math.Min(Priority, 10) / 100; return MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + severityFactor * distanceFactor, 0, 1)); } - public override bool IsCompleted() => targetHull.FireSources.None(); - - public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveExtinguishFire otherExtinguishFire && otherExtinguishFire.targetHull == targetHull; + protected override bool Check() => targetHull.FireSources.None(); protected override void Act(float deltaTime) { @@ -58,9 +63,9 @@ namespace Barotrauma if (extinguisher == null) { #if DEBUG - DebugConsole.ThrowError("AIObjectiveExtinguishFire failed - the item \"" + extinguisherItem + "\" has no RepairTool component but is tagged as an extinguisher"); + DebugConsole.ThrowError($"{character.Name}: AIObjectiveExtinguishFire failed - the item \"" + extinguisherItem + "\" has no RepairTool component but is tagged as an extinguisher"); #endif - abandon = true; + Abandon = true; return; } foreach (FireSource fs in targetHull.FireSources) @@ -119,7 +124,9 @@ namespace Barotrauma if (move) { //go to the first firesource - TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager)); + TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager), + onAbandon: () => Abandon = true, + onCompleted: () => RemoveSubObjective(ref gotoObjective)); } break; } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs index 14e6bb13b..f5142278c 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs @@ -9,6 +9,7 @@ namespace Barotrauma { public override string DebugTag => "extinguish fires"; public override bool ForceRun => true; + public override bool IgnoreUnsafeHulls => true; public AIObjectiveExtinguishFires(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { } @@ -18,7 +19,6 @@ namespace Barotrauma public static float GetFireSeverity(Hull hull) => hull.FireSources.Sum(fs => fs.Size.X); - public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveExtinguishFires; protected override IEnumerable GetList() => Hull.hullList; protected override AIObjective ObjectiveConstructor(Hull target) diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFightIntruders.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFightIntruders.cs index 162d0f5e5..d6c965eed 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFightIntruders.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFightIntruders.cs @@ -11,12 +11,11 @@ namespace Barotrauma { public override string DebugTag => "fight intruders"; protected override float IgnoreListClearInterval => 30; + public virtual bool IgnoreUnsafeHulls => true; public AIObjectiveFightIntruders(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { } - public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveFightIntruders; - protected override bool Filter(Character target) => IsValidTarget(target, character); protected override IEnumerable GetList() => Character.CharacterList; diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs index 33b62d9d0..9a7959b4e 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFindDivingGear.cs @@ -1,53 +1,56 @@ using Barotrauma.Items.Components; using Microsoft.Xna.Framework; -using System.Linq; using Barotrauma.Extensions; namespace Barotrauma { class AIObjectiveFindDivingGear : AIObjective { - public override string DebugTag => "find diving gear"; + public override string DebugTag => $"find diving gear ({gearTag})"; public override bool ForceRun => true; + public override bool KeepDivingGearOn => true; + public override bool IgnoreUnsafeHulls => true; private readonly string gearTag; + private readonly string fallbackTag; private AIObjectiveGetItem getDivingGear; private AIObjectiveContainItem getOxygen; - public override bool IsCompleted() - { - for (int i = 0; i < character.Inventory.Items.Length; i++) - { - if (character.Inventory.SlotTypes[i] == InvSlotType.Any || character.Inventory.Items[i] == null) { continue; } - if (character.Inventory.Items[i].HasTag(gearTag)) - { - var containedItems = character.Inventory.Items[i].ContainedItems; - if (containedItems == null) { continue; } - return containedItems.Any(it => (it.Prefab.Identifier == "oxygentank" || it.HasTag("oxygensource")) && it.Condition > 0.0f); - } - } - return false; - } + public static float lowOxygenThreshold = 10; - public override float GetPriority() => MathHelper.Clamp(100 - character.OxygenAvailable, 0, 100); - public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveFindDivingGear; + protected override bool Check() => HumanAIController.HasItem(character, gearTag, "oxygensource") || HumanAIController.HasItem(character, fallbackTag, "oxygensource"); public AIObjectiveFindDivingGear(Character character, bool needDivingSuit, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { - gearTag = needDivingSuit ? "divingsuit" : "diving"; + gearTag = needDivingSuit ? "divingsuit" : "divingmask"; + fallbackTag = needDivingSuit ? "divingsuit" : "diving"; } protected override void Act(float deltaTime) { - var item = character.Inventory.FindItemByTag(gearTag); + if (character.LockHands) + { + Abandon = true; + return; + } + var item = character.Inventory.FindItemByIdentifier(gearTag, true) ?? character.Inventory.FindItemByTag(gearTag, true); + if (item == null && fallbackTag != gearTag) + { + item = character.Inventory.FindItemByTag(fallbackTag, true); + } if (item == null || !character.HasEquippedItem(item)) { TryAddSubObjective(ref getDivingGear, () => { - character.Speak(TextManager.Get("DialogGetDivingGear"), null, 0.0f, "getdivinggear", 30.0f); - return new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true); - }); + if (item == null) + { + character.Speak(TextManager.Get("DialogGetDivingGear"), null, 0.0f, "getdivinggear", 30.0f); + } + return new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true) { AllowToFindDivingGear = false }; + }, + onAbandon: () => Abandon = true, + onCompleted: () => RemoveSubObjective(ref getDivingGear)); } else { @@ -55,9 +58,9 @@ namespace Barotrauma if (containedItems == null) { #if DEBUG - DebugConsole.ThrowError("AIObjectiveFindDivingGear failed - the item \"" + item + "\" has no proper inventory"); + DebugConsole.ThrowError($"{character.Name}: AIObjectiveFindDivingGear failed - the item \"" + item + "\" has no proper inventory"); #endif - abandon = true; + Abandon = true; return; } // Drop empty tanks @@ -69,13 +72,54 @@ namespace Barotrauma containedItem.Drop(character); } } - if (containedItems.None(it => (it.Prefab.Identifier == "oxygentank" || it.HasTag("oxygensource")) && it.Condition > 0.0f)) + if (containedItems.None(it => it.HasTag("oxygensource") && it.Condition > lowOxygenThreshold)) { - TryAddSubObjective(ref getOxygen, () => + var oxygenTank = character.Inventory.FindItemByTag("oxygensource", true); + if (oxygenTank != null) { - character.Speak(TextManager.Get("DialogGetOxygenTank"), null, 0, "getoxygentank", 30.0f); - return new AIObjectiveContainItem(character, new string[] { "oxygentank", "oxygensource" }, item.GetComponent(), objectiveManager); - }); + var container = item.GetComponent(); + if (container.Item.ParentInventory == character.Inventory) + { + character.Inventory.RemoveItem(oxygenTank); + if (!container.Inventory.TryPutItem(oxygenTank, null)) + { + oxygenTank.Drop(character); + Abandon = true; + } + } + else + { + container.Combine(oxygenTank, character); + } + } + else + { + // Seek oxygen that has min 10% condition left + TryAddSubObjective(ref getOxygen, () => + { + character.Speak(TextManager.Get("DialogGetOxygenTank"), null, 0, "getoxygentank", 30.0f); + return new AIObjectiveContainItem(character, new string[] { "oxygensource" }, item.GetComponent(), objectiveManager) + { + AllowToFindDivingGear = false, + ConditionLevel = lowOxygenThreshold + }; + }, + onAbandon: () => + { + // Try to seek any oxygen sources + TryAddSubObjective(ref getOxygen, () => + { + return new AIObjectiveContainItem(character, new string[] { "oxygensource" }, item.GetComponent(), objectiveManager) + { + AllowToFindDivingGear = false, + ConditionLevel = 0 + }; + }, + onAbandon: () => Abandon = true, + onCompleted: () => RemoveSubObjective(ref getOxygen)); + }, + onCompleted: () => RemoveSubObjective(ref getOxygen)); + } } } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFindSafety.cs index af186326e..1fdd72e69 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -9,17 +9,18 @@ namespace Barotrauma { public override string DebugTag => "find safety"; public override bool ForceRun => true; + public override bool KeepDivingGearOn => true; + public override bool IgnoreUnsafeHulls => true; + public override bool ConcurrentObjectives => true; + public override bool IsLoop { get => true; set => throw new System.Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace); } // TODO: expose? const float priorityIncrease = 100; const float priorityDecrease = 10; const float SearchHullInterval = 3.0f; - const float clearUnreachableInterval = 30; - - public readonly HashSet unreachable = new HashSet(); private float currenthullSafety; - private float unreachableClearTimer; + private float searchHullTimer; private AIObjectiveGoTo goToObjective; @@ -27,21 +28,18 @@ namespace Barotrauma public AIObjectiveFindSafety(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { } - public override bool IsCompleted() => false; + protected override bool Check() => false; public override bool CanBeCompleted => true; - public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveFindSafety; + private bool resetPriority; public override void Update(float deltaTime) { - if (unreachableClearTimer > 0) + if (resetPriority) { - unreachableClearTimer -= deltaTime; - } - else - { - unreachableClearTimer = clearUnreachableInterval; - unreachable.Clear(); + Priority = 0; + resetPriority = false; + return; } if (character.CurrentHull == null) { @@ -49,7 +47,10 @@ namespace Barotrauma Priority = objectiveManager.CurrentOrder is AIObjectiveGoTo ? 0 : 100; return; } - if (character.OxygenAvailable < CharacterHealth.LowOxygenThreshold) { Priority = 100; } + if (HumanAIController.NeedsDivingGear(character, character.CurrentHull, out _) && !HumanAIController.HasDivingGear(character)) + { + Priority = 100; + } currenthullSafety = HumanAIController.CurrentHullSafety; if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD) { @@ -61,7 +62,7 @@ namespace Barotrauma Priority += dangerFactor * priorityIncrease * deltaTime; } Priority = MathHelper.Clamp(Priority, 0, 100); - if (divingGearObjective != null && !divingGearObjective.IsCompleted() && divingGearObjective.CanBeCompleted) + if (divingGearObjective != null && !divingGearObjective.IsCompleted && divingGearObjective.CanBeCompleted) { // Boost the priority while seeking the diving gear Priority = Math.Max(Priority, Math.Min(AIObjectiveManager.OrderPriority + 20, 100)); @@ -72,32 +73,36 @@ namespace Barotrauma private Hull previousSafeHull; protected override void Act(float deltaTime) { - var currentHull = character.AnimController.CurrentHull; - bool needsDivingGear = HumanAIController.NeedsDivingGear(currentHull); - bool needsDivingSuit = needsDivingGear && (currentHull == null || currentHull.WaterPercentage > 90); + var currentHull = character.CurrentHull; + bool needsDivingGear = HumanAIController.NeedsDivingGear(character, currentHull, out bool needsDivingSuit); bool needsEquipment = false; if (needsDivingSuit) { - needsEquipment = !HumanAIController.HasDivingSuit(character); + needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.lowOxygenThreshold); } else if (needsDivingGear) { - needsEquipment = !HumanAIController.HasDivingMask(character); + needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.lowOxygenThreshold); } - if (needsEquipment) + if (needsEquipment && divingGearObjective == null && !character.LockHands) { - TryAddSubObjective(ref divingGearObjective, - () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager), - onAbandon: () => searchHullTimer = Math.Min(1, searchHullTimer)); + RemoveSubObjective(ref goToObjective); + TryAddSubObjective(ref divingGearObjective, + constructor: () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager), + onAbandon: () => + { + searchHullTimer = Math.Min(1, searchHullTimer); + // Don't reset the diving gear objective, because it's possible that there is no diving gear -> seek a safe hull and then reset so that we can check again. + }, + onCompleted: () => + { + resetPriority = true; + searchHullTimer = Math.Min(1, searchHullTimer); + RemoveSubObjective(ref divingGearObjective); + }); } - else + else if (divingGearObjective == null || !divingGearObjective.CanBeCompleted) { - if (divingGearObjective != null && divingGearObjective.IsCompleted()) - { - // Reset the devotion. - Priority = 0; - divingGearObjective = null; - } if (currenthullSafety < HumanAIController.HULL_SAFETY_THRESHOLD) { searchHullTimer = Math.Min(1, searchHullTimer); @@ -108,7 +113,7 @@ namespace Barotrauma } else { - searchHullTimer = SearchHullInterval; + searchHullTimer = SearchHullInterval * Rand.Range(0.9f, 1.1f); previousSafeHull = currentSafeHull; currentSafeHull = FindBestHull(); if (currentSafeHull == null) @@ -119,70 +124,80 @@ namespace Barotrauma { if (goToObjective?.Target != currentSafeHull) { - goToObjective = null; + RemoveSubObjective(ref goToObjective); } TryAddSubObjective(ref goToObjective, constructor: () => new AIObjectiveGoTo(currentSafeHull, character, objectiveManager, getDivingGearIfNeeded: true) { - AllowGoingOutside = HumanAIController.HasDivingSuit(character) - }, - onAbandon: () => unreachable.Add(goToObjective.Target as Hull)); + AllowGoingOutside = HumanAIController.HasDivingSuit(character, conditionPercentage: 50) + }, + onCompleted: () => + { + if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD || + HumanAIController.NeedsDivingGear(character, currentHull, out bool needsSuit) && (needsSuit ? HumanAIController.HasDivingSuit(character) : HumanAIController.HasDivingMask(character))) + { + resetPriority = true; + searchHullTimer = Math.Min(1, searchHullTimer); + } + RemoveSubObjective(ref goToObjective); + // If diving gear objective failed, let's reset it here. + RemoveSubObjective(ref divingGearObjective); + }, + onAbandon: () => + { + if (currentHull != null) + { + HumanAIController.UnreachableHulls.Add(goToObjective.Target as Hull); + } + RemoveSubObjective(ref goToObjective); + }); } else { - goToObjective = null; + RemoveSubObjective(ref goToObjective); } } - if (goToObjective != null) + if (subObjectives.Any(so => so.CanBeCompleted)) { return; } + if (currentHull != null) { - if (goToObjective.IsCompleted()) + //goto objective doesn't exist (a safe hull not found, or a path to a safe hull not found) + // -> attempt to manually steer away from hazards + Vector2 escapeVel = Vector2.Zero; + // TODO: optimize + foreach (FireSource fireSource in HumanAIController.VisibleHulls.SelectMany(h => h.FireSources)) { - objectiveManager.GetObjective()?.Wander(deltaTime); - } - Priority = 0; - return; - } - if (currentHull == null) { return; } - //goto objective doesn't exist (a safe hull not found, or a path to a safe hull not found) - // -> attempt to manually steer away from hazards - Vector2 escapeVel = Vector2.Zero; - // TODO: optimize - foreach (FireSource fireSource in HumanAIController.VisibleHulls.SelectMany(h => h.FireSources)) - { - Vector2 dir = character.Position - fireSource.Position; - float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(fireSource.Position, character.Position), 0.1f, 10.0f); - escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier); - } - foreach (Character enemy in Character.CharacterList) - { - if (enemy.IsDead || enemy.IsUnconscious || enemy.Removed || HumanAIController.IsFriendly(enemy)) { continue; } - if (HumanAIController.VisibleHulls.Contains(enemy.CurrentHull)) - { - Vector2 dir = character.Position - enemy.Position; - float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(enemy.Position, character.Position), 0.1f, 10.0f); + Vector2 dir = character.Position - fireSource.Position; + float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(fireSource.Position, character.Position), 0.1f, 10.0f); escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier); } - } - if (escapeVel != Vector2.Zero) - { - float left = currentHull.Rect.X + 50; - float right = currentHull.Rect.Right - 50; - //only move if we haven't reached the edge of the room - if (escapeVel.X < 0 && character.Position.X > left || escapeVel.X > 0 && character.Position.X < right) + foreach (Character enemy in Character.CharacterList) { - character.AIController.SteeringManager.SteeringManual(deltaTime, escapeVel); + if (HumanAIController.IsFriendly(enemy) || !HumanAIController.IsActive(enemy)) { continue; } + if (HumanAIController.VisibleHulls.Contains(enemy.CurrentHull)) + { + Vector2 dir = character.Position - enemy.Position; + float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(enemy.Position, character.Position), 0.1f, 10.0f); + escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier); + } } - else + if (escapeVel != Vector2.Zero) { - character.AnimController.TargetDir = escapeVel.X < 0.0f ? Direction.Right : Direction.Left; - character.AIController.SteeringManager.Reset(); + float left = currentHull.Rect.X + 50; + float right = currentHull.Rect.Right - 50; + //only move if we haven't reached the edge of the room + if (escapeVel.X < 0 && character.Position.X > left || escapeVel.X > 0 && character.Position.X < right) + { + character.AIController.SteeringManager.SteeringManual(deltaTime, escapeVel); + } + else + { + character.AnimController.TargetDir = escapeVel.X < 0.0f ? Direction.Right : Direction.Left; + character.AIController.SteeringManager.Reset(); + } + return; } } - else - { - Priority = 0; - objectiveManager.GetObjective()?.Wander(deltaTime); - } + objectiveManager.GetObjective().Wander(deltaTime); } } @@ -195,24 +210,28 @@ namespace Barotrauma if (hull.Submarine == null) { continue; } if (!allowChangingTheSubmarine && hull.Submarine != character.Submarine) { continue; } if (ignoredHulls != null && ignoredHulls.Contains(hull)) { continue; } - if (unreachable.Contains(hull)) { continue; } + if (HumanAIController.UnreachableHulls.Contains(hull)) { continue; } float hullSafety = 0; if (character.CurrentHull != null && character.Submarine != null) { // Inside if (!character.Submarine.IsConnectedTo(hull.Submarine)) { continue; } - hullSafety = HumanAIController.GetHullSafety(hull, character); - // Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally) - float dist = Math.Abs(character.WorldPosition.X - hull.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y) * 2.0f; + hullSafety = HumanAIController.GetHullSafety(hull, hull.GetConnectedHulls(true, 1), character); + float yDist = Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y); + yDist = yDist > 100 ? yDist * 3 : 0; + float dist = Math.Abs(character.WorldPosition.X - hull.WorldPosition.X) + yDist; float distanceFactor = MathHelper.Lerp(1, 0.9f, MathUtils.InverseLerp(0, 10000, dist)); hullSafety *= distanceFactor; //skip the hull if the safety is already less than the best hull //(no need to do the expensive pathfinding if we already know we're not going to choose this hull) if (hullSafety < bestValue) { continue; } - var path = PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition); - if (path.Unreachable) + // Don't allow to go outside if not already outside. + var path = character.CurrentHull != null ? + PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition, nodeFilter: node => node.Waypoint.CurrentHull != null) : + PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition); + if (path.Unreachable && character.CurrentHull != null) { - unreachable.Add(hull); + HumanAIController.UnreachableHulls.Add(hull); continue; } // Each unsafe node reduces the hull safety value. diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFixLeak.cs index 9f152637d..6adc610b0 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -11,10 +11,10 @@ namespace Barotrauma { public override string DebugTag => "fix leak"; public override bool ForceRun => true; + public override bool KeepDivingGearOn => true; public Gap Leak { get; private set; } - private AIObjectiveFindDivingGear findDivingGear; private AIObjectiveGetItem getWeldingTool; private AIObjectiveContainItem refuelObjective; private AIObjectiveGoTo gotoObjective; @@ -25,43 +25,30 @@ namespace Barotrauma Leak = leak; } - public override bool IsCompleted() - { - return Leak.Open <= 0.0f || Leak.Removed; - } + protected override bool Check() => Leak.Open <= 0 || Leak.Removed; public override float GetPriority() { - if (Leak.Open == 0.0f) { return 0.0f; } - // Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally) - float dist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y) * 2.0f; - float distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 10000, dist)); - float severity = AIObjectiveFixLeaks.GetLeakSeverity(Leak); + if (Leak.Removed || Leak.Open <= 0) { return 0; } + float xDist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X); + float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y); + // Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally). + // If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby. + float distanceFactor = xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, xDist + yDist * 3.0f)); + float severity = AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100; float max = Math.Min((AIObjectiveManager.OrderPriority - 1), 90); float devotion = Math.Min(Priority, 10) / 100; return MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + severity * distanceFactor * PriorityModifier, 0, 1)); } - public override bool IsDuplicate(AIObjective otherObjective) - { - if (!(otherObjective is AIObjectiveFixLeak fixLeak)) { return false; } - return fixLeak.Leak == Leak; - } - protected override void Act(float deltaTime) { - if (!Leak.IsRoomToRoom) - { - if (!HumanAIController.HasDivingSuit(character)) - { - TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, true, objectiveManager)); - return; - } - } - var weldingTool = character.Inventory.FindItemByTag("weldingtool"); + var weldingTool = character.Inventory.FindItemByTag("weldingtool", true); if (weldingTool == null) { - TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingtool", objectiveManager, true)); + TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingtool", objectiveManager, true), + onAbandon: () => Abandon = true, + onCompleted: () => RemoveSubObjective(ref getWeldingTool)); return; } else @@ -70,9 +57,9 @@ namespace Barotrauma if (containedItems == null) { #if DEBUG - DebugConsole.ThrowError("AIObjectiveFixLeak failed - the item \"" + weldingTool + "\" has no proper inventory"); + DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"" + weldingTool + "\" has no proper inventory"); #endif - abandon = true; + Abandon = true; return; } // Drop empty tanks @@ -84,9 +71,11 @@ namespace Barotrauma containedItem.Drop(character); } } - if (containedItems.None(i => i.HasTag("weldingfueltank") && i.Condition > 0.0f)) + if (containedItems.None(i => i.HasTag("weldingfuel") && i.Condition > 0.0f)) { - TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfueltank", weldingTool.GetComponent(), objectiveManager)); + TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent(), objectiveManager), + onAbandon: () => Abandon = true, + onCompleted: () => RemoveSubObjective(ref refuelObjective)); return; } } @@ -95,30 +84,55 @@ namespace Barotrauma if (repairTool == null) { #if DEBUG - DebugConsole.ThrowError("AIObjectiveFixLeak failed - the item \"" + weldingTool + "\" has no RepairTool component but is tagged as a welding tool"); + DebugConsole.ThrowError($"{character.Name}: AIObjectiveFixLeak failed - the item \"" + weldingTool + "\" has no RepairTool component but is tagged as a welding tool"); #endif - abandon = true; + Abandon = true; return; } - Vector2 gapDiff = Leak.WorldPosition - character.WorldPosition; + Vector2 toLeak = Leak.WorldPosition - character.WorldPosition; // TODO: use the collider size/reach? - if (!character.AnimController.InWater && Math.Abs(gapDiff.X) < 100 && gapDiff.Y < 0.0f && gapDiff.Y > -150) + if (!character.AnimController.InWater && Math.Abs(toLeak.X) < 100 && toLeak.Y < 0.0f && toLeak.Y > -150) { HumanAIController.AnimController.Crouching = true; } - // Use a greater reach, because the distance is calculated from the character to the leak, not from the item to the leak. - float reach = repairTool.Range + ((HumanoidAnimController)character.AnimController).ArmLength; - bool canOperate = gapDiff.LengthSquared() < reach * reach; + float reach = repairTool.Range + ConvertUnits.ToDisplayUnits(((HumanoidAnimController)character.AnimController).ArmLength); + bool canOperate = toLeak.LengthSquared() < reach * reach; if (canOperate) { - TryAddSubObjective(ref operateObjective, () => new AIObjectiveOperateItem(repairTool, character, objectiveManager, option: "", requireEquip: true, operateTarget: Leak)); + TryAddSubObjective(ref operateObjective, () => new AIObjectiveOperateItem(repairTool, character, objectiveManager, option: "", requireEquip: true, operateTarget: Leak), + onAbandon: () => Abandon = true, + onCompleted: () => + { + if (Check()) { IsCompleted = true; } + else + { + // Failed to operate. Probably too far. + Abandon = true; + } + }); } else { TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(Leak, character, objectiveManager) { + AllowGoingOutside = objectiveManager.IsCurrentOrder(), CloseEnough = reach - }); + }, + onAbandon: () => + { + if (Check()) { IsCompleted = true; } + else if ((Leak.WorldPosition - character.WorldPosition).LengthSquared() > reach * reach * 2) + { + // Too far + Abandon = true; + } + else + { + // We are close, try again. + RemoveSubObjective(ref gotoObjective); + } + }, + onCompleted: () => RemoveSubObjective(ref gotoObjective)); } } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFixLeaks.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFixLeaks.cs index 255cc6c3b..97639c8a0 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFixLeaks.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveFixLeaks.cs @@ -10,6 +10,8 @@ namespace Barotrauma { public override string DebugTag => "fix leaks"; public override bool ForceRun => true; + public override bool KeepDivingGearOn => true; + public override bool IgnoreUnsafeHulls => true; public AIObjectiveFixLeaks(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { } @@ -18,7 +20,7 @@ namespace Barotrauma public static float GetLeakSeverity(Gap leak) { if (leak == null) { return 0; } - float sizeFactor = MathHelper.Lerp(1, 10, MathUtils.InverseLerp(0, 200, (leak.IsHorizontal ? leak.Rect.Width : leak.Rect.Height))); + float sizeFactor = MathHelper.Lerp(1, 10, MathUtils.InverseLerp(0, 200, leak.Size)); float severity = sizeFactor * leak.Open; if (!leak.IsRoomToRoom) { @@ -32,7 +34,6 @@ namespace Barotrauma } } - public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveFixLeaks; protected override float TargetEvaluation() => Targets.Max(t => GetLeakSeverity(t)); protected override IEnumerable GetList() => Gap.GapList; protected override AIObjective ObjectiveConstructor(Gap gap) diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveGetItem.cs index 7a752c48d..756a2fee8 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -26,6 +26,8 @@ namespace Barotrauma private AIObjectiveGoTo goToObjective; private float currItemPriority; + public bool AllowToFindDivingGear { get; set; } = true; + public override float GetPriority() { if (objectiveManager.CurrentOrder == this) @@ -35,7 +37,7 @@ namespace Barotrauma return 1.0f; } - public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = false, float priorityModifier = 1) + public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { currSearchIndex = -1; @@ -43,10 +45,10 @@ namespace Barotrauma this.targetItem = targetItem; } - public AIObjectiveGetItem(Character character, string itemIdentifier, AIObjectiveManager objectiveManager, bool equip = false, bool checkInventory = true, float priorityModifier = 1) + public AIObjectiveGetItem(Character character, string itemIdentifier, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1) : this(character, new string[] { itemIdentifier }, objectiveManager, equip, checkInventory, priorityModifier) { } - public AIObjectiveGetItem(Character character, string[] itemIdentifiers, AIObjectiveManager objectiveManager, bool equip = false, bool checkInventory = true, float priorityModifier = 1) + public AIObjectiveGetItem(Character character, string[] itemIdentifiers, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { currSearchIndex = -1; @@ -65,32 +67,11 @@ namespace Barotrauma private void CheckInventory() { if (itemIdentifiers == null) { return; } - for (int i = 0; i < character.Inventory.Items.Length; i++) + var item = character.Inventory.FindItem(i => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id)) && i.Condition > 0, recursive: true); + if (item != null) { - if (character.Inventory.Items[i] == null || character.Inventory.Items[i].Condition <= 0.0f) { continue; } - if (itemIdentifiers.Any(id => character.Inventory.Items[i].Prefab.Identifier == id || character.Inventory.Items[i].HasTag(id))) - { - targetItem = character.Inventory.Items[i]; - moveToTarget = targetItem; - currItemPriority = 100.0f; - break; - } - //check items inside items (tool inside a toolbox etc) - var containedItems = character.Inventory.Items[i].ContainedItems; - if (containedItems != null) - { - foreach (Item containedItem in containedItems) - { - if (containedItem == null || containedItem.Condition <= 0.0f) { continue; } - if (itemIdentifiers.Any(id => containedItem.Prefab.Identifier == id || containedItem.HasTag(id))) - { - targetItem = containedItem; - moveToTarget = character.Inventory.Items[i]; - currItemPriority = 100.0f; - break; - } - } - } + targetItem = item; + moveToTarget = item.GetRootContainer() ?? item; } } @@ -98,58 +79,91 @@ namespace Barotrauma { if (character.LockHands) { - abandon = true; + Abandon = true; return; } - - FindTargetItem(); - if (targetItem == null || moveToTarget == null) + if (targetItem == null) { - objectiveManager.GetObjective()?.Wander(deltaTime); - return; + FindTargetItem(); + if (targetItem == null || moveToTarget == null) + { + if (targetItem != null && moveToTarget == null) + { +#if DEBUG + DebugConsole.ThrowError($"{character.Name}: Move to target is null!"); +#endif + Abandon = true; + } + objectiveManager.GetObjective().Wander(deltaTime); + return; + } + } + if (character.IsItemTakenBySomeoneElse(targetItem)) + { +#if DEBUG + DebugConsole.NewMessage($"{character.Name}: Found an item, but it's already equipped by someone else. Aborting.", Color.Yellow); +#endif + Abandon = true; } if (character.CanInteractWith(targetItem, out _, checkLinked: false)) { - if (IsTakenBySomeone(targetItem)) + var pickable = targetItem.GetComponent(); + if (pickable == null) { #if DEBUG - DebugConsole.NewMessage($"{character.Name}: Found an item, but it's equipped by someone else. Aborting.", Color.Yellow); + DebugConsole.NewMessage($"{character.Name}: Target not pickable. Aborting.", Color.Yellow); #endif - abandon = true; + Abandon = true; + return; + } + targetItem.TryInteract(character, forceSelectKey: true); + if (equip) + { + int targetSlot = -1; + //check if all the slots required by the item are free + foreach (InvSlotType slots in pickable.AllowedSlots) + { + if (slots.HasFlag(InvSlotType.Any)) { continue; } + for (int i = 0; i < character.Inventory.Items.Length; i++) + { + //slot not needed by the item, continue + if (!slots.HasFlag(character.Inventory.SlotTypes[i])) { continue; } + targetSlot = i; + //slot free, continue + var otherItem = character.Inventory.Items[i]; + if (otherItem == null) { continue; } + //try to move the existing item to LimbSlot.Any and continue if successful + if (character.Inventory.TryPutItem(otherItem, character, new List() { InvSlotType.Any })) { continue; } + //if everything else fails, simply drop the existing item + otherItem.Drop(character); + } + } + if (character.Inventory.TryPutItem(targetItem, targetSlot, false, false, character)) + { + IsCompleted = true; + } + else + { +#if DEBUG + DebugConsole.NewMessage($"{character.Name}: Failed to equip/move the item '{targetItem.Name}' into the character inventory. Aborting.", Color.Red); +#endif + Abandon = true; + } } else { - int targetSlot = -1; - if (equip) + targetItem.ParentInventory.RemoveItem(targetItem); + if (character.Inventory.TryPutItem(targetItem, null, new List() { InvSlotType.Any })) { - var pickable = targetItem.GetComponent(); - if (pickable == null) - { - abandon = true; - return; - } - //check if all the slots required by the item are free - foreach (InvSlotType slots in pickable.AllowedSlots) - { - if (slots.HasFlag(InvSlotType.Any)) { continue; } - for (int i = 0; i < character.Inventory.Items.Length; i++) - { - //slot not needed by the item, continue - if (!slots.HasFlag(character.Inventory.SlotTypes[i])) { continue; } - targetSlot = i; - //slot free, continue - if (character.Inventory.Items[i] == null) { continue; } - //try to move the existing item to LimbSlot.Any and continue if successful - if (character.Inventory.TryPutItem(character.Inventory.Items[i], character, new List() { InvSlotType.Any })) { continue; } - //if everything else fails, simply drop the existing item - character.Inventory.Items[i].Drop(character); - } - } + IsCompleted = true; } - targetItem.TryInteract(character, false, true); - if (targetSlot > -1 && !character.HasEquippedItem(targetItem)) + else { - character.Inventory.TryPutItem(targetItem, targetSlot, false, false, character); + Abandon = true; +#if DEBUG + DebugConsole.NewMessage($"{character.Name}: Failed to equip/move the item '{targetItem.Name}' into the character inventory. Aborting.", Color.Red); +#endif + targetItem.Drop(character); } } } @@ -158,17 +172,16 @@ namespace Barotrauma TryAddSubObjective(ref goToObjective, constructor: () => { - //check if we're already looking for a diving gear - bool gettingDivingGear = (targetItem != null && targetItem.Prefab.Identifier == "divingsuit" || targetItem.HasTag("diving")) || - (itemIdentifiers != null && (itemIdentifiers.Contains("diving") || itemIdentifiers.Contains("divingsuit"))); - return new AIObjectiveGoTo(moveToTarget, character, objectiveManager, repeat: false, getDivingGearIfNeeded: !gettingDivingGear); + return new AIObjectiveGoTo(moveToTarget, character, objectiveManager, repeat: false, getDivingGearIfNeeded: AllowToFindDivingGear); }, onAbandon: () => { targetItem = null; moveToTarget = null; ignoredItems.Add(targetItem); - }); + RemoveSubObjective(ref goToObjective); + }, + onCompleted: () => RemoveSubObjective(ref goToObjective)); } } @@ -182,9 +195,9 @@ namespace Barotrauma if (targetItem == null) { #if DEBUG - DebugConsole.NewMessage($"{character.Name}: Cannot find the item, because neither identifiers nor item is was defined.", Color.Red); + DebugConsole.NewMessage($"{character.Name}: Cannot find the item, because neither identifiers nor item was defined.", Color.Red); #endif - abandon = true; + Abandon = true; } return; } @@ -194,26 +207,29 @@ namespace Barotrauma var item = Item.ItemList[currSearchIndex]; if (ignoredItems.Contains(item)) { continue; } if (item.Submarine == null) { continue; } - else if (item.Submarine.TeamID != character.TeamID) { continue; } - else if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { continue; } + if (item.Submarine.TeamID != character.TeamID) { continue; } + if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { continue; } if (item.CurrentHull == null || item.Condition <= 0.0f) { 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 (IsTakenBySomeone(item)) { continue; } - float itemPriority = 0.0f; + if (character.IsItemTakenBySomeoneElse(item)) { continue; } + float itemPriority = 1; if (GetItemPriority != null) { - //ignore if the item has zero priority itemPriority = GetItemPriority(item); - if (itemPriority <= 0.0f) { continue; } } Item rootContainer = item.GetRootContainer(); - itemPriority -= Vector2.Distance((rootContainer ?? item).Position, character.Position) * 0.01f; + Vector2 itemPos = (rootContainer ?? item).WorldPosition; + float yDist = Math.Abs(character.WorldPosition.Y - itemPos.Y); + yDist = yDist > 100 ? yDist * 5 : 0; + float dist = Math.Abs(character.WorldPosition.X - itemPos.X) + yDist; + float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 10000, dist)); + itemPriority *= distanceFactor; //ignore if the item has a lower priority than the currently selected one - if (moveToTarget != null && itemPriority < currItemPriority) { continue; } + if (itemPriority < currItemPriority) { continue; } currItemPriority = itemPriority; targetItem = item; moveToTarget = rootContainer ?? item; @@ -222,82 +238,29 @@ namespace Barotrauma if (currSearchIndex >= Item.ItemList.Count - 1 && targetItem == null) { #if DEBUG - DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s): {string.Join(", ", itemIdentifiers)}", Color.Red); + DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s): {string.Join(", ", itemIdentifiers)}", Color.Yellow); #endif - abandon = true; + Abandon = true; } } - public override bool IsDuplicate(AIObjective otherObjective) - { - if (!(otherObjective is AIObjectiveGetItem getItem)) { return false; } - if (getItem.equip != equip) { return false; } - if (getItem.itemIdentifiers != null && itemIdentifiers != null) - { - if (getItem.itemIdentifiers.Length != itemIdentifiers.Length) { return false; } - for (int i = 0; i < getItem.itemIdentifiers.Length; i++) - { - if (getItem.itemIdentifiers[i] != itemIdentifiers[i]) { return false; } - } - return true; - } - else if (getItem.itemIdentifiers == null && itemIdentifiers == null) - { - return getItem.targetItem == targetItem; - } - return false; - } - - public override bool IsCompleted() + protected override bool Check() { + if (IsCompleted) { return true; } if (targetItem != null) { - return HasItem(targetItem); + return character.HasItem(targetItem, equip); } else if (itemIdentifiers != null) { - foreach (string itemName in itemIdentifiers) + var matchingItem = character.Inventory.FindItem(i => !ignoredItems.Contains(i) && itemIdentifiers.Any(id => id == i.Prefab.Identifier || i.HasTag(id)), recursive: true); + if (matchingItem != null) { - var matchingItem = character.Inventory.FindItemByTag(itemName) ?? character.Inventory.FindItemByIdentifier(itemName); - if (matchingItem != null && (!equip || character.HasEquippedItem(matchingItem))) - { - return true; - } + return !equip || character.HasEquippedItem(matchingItem); } return false; } return false; } - - private bool HasItem(Item item) - { - bool isEquipped = !equip || character.HasEquippedItem(item); - if (character.Inventory.Items.Contains(item) && isEquipped) { return true; } - if (!equip) - { - Item rootContainer = item.GetRootContainer(); - if (rootContainer != null && rootContainer.ParentInventory is CharacterInventory) - { - return rootContainer.ParentInventory.Owner == character; - } - } - return false; - } - - private bool IsTakenBySomeone(Item item) - { - //if the item is inside a character's inventory, don't steal it unless the character is dead - if (item.ParentInventory is CharacterInventory) - { - if (item.ParentInventory.Owner is Character owner && owner != character && !owner.IsDead) { return true; } - } - //if the item is inside an item, which is inside a character's inventory, don't steal it unless the character is dead - Item rootContainer = item.GetRootContainer(); - if (rootContainer != null && rootContainer.ParentInventory is CharacterInventory) - { - if (rootContainer.ParentInventory.Owner is Character owner && owner != character && !owner.IsDead) { return true; } - } - return false; - } } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveGoTo.cs index 34e1a4048..f2ccf61ef 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -1,5 +1,6 @@ using Microsoft.Xna.Framework; using System; +using System.Linq; using Barotrauma.Extensions; namespace Barotrauma @@ -9,7 +10,7 @@ namespace Barotrauma public override string DebugTag => "go to"; private AIObjectiveFindDivingGear findDivingGear; - private bool repeat; + private readonly bool repeat; //how long until the path to the target is declared unreachable private float waitUntilPathUnreachable; private bool getDivingGearIfNeeded; @@ -21,13 +22,23 @@ namespace Barotrauma public bool followControlledCharacter; public bool mimic; + private float _closeEnough = 50; /// /// Display units /// - public float CloseEnough { get; set; } = 50; + public float CloseEnough + { + get { return _closeEnough; } + set + { + _closeEnough = Math.Max(_closeEnough, value); + } + } public bool IgnoreIfTargetDead { get; set; } public bool AllowGoingOutside { get; set; } + public override bool AbandonWhenCannotCompleteSubjectives => !repeat; + public ISpatialEntity Target { get; private set; } public override float GetPriority() @@ -42,14 +53,18 @@ namespace Barotrauma return 1.0f; } - public AIObjectiveGoTo(ISpatialEntity target, Character character, AIObjectiveManager objectiveManager, bool repeat = false, bool getDivingGearIfNeeded = true, float priorityModifier = 1) + public AIObjectiveGoTo(ISpatialEntity target, Character character, AIObjectiveManager objectiveManager, bool repeat = false, bool getDivingGearIfNeeded = true, float priorityModifier = 1, float closeEnough = 0) : base (character, objectiveManager, priorityModifier) { this.Target = target; this.repeat = repeat; waitUntilPathUnreachable = 3.0f; this.getDivingGearIfNeeded = getDivingGearIfNeeded; - CalculateCloseEnough(); + CloseEnough = closeEnough; + if (Target is Item i) + { + CloseEnough = Math.Max(CloseEnough, i.InteractDistance + Math.Max(i.Rect.Width, i.Rect.Height) / 2); + } } protected override void Act(float deltaTime) @@ -58,17 +73,17 @@ namespace Barotrauma { if (Character.Controlled == null) { - abandon = true; + Abandon = true; return; } Target = Character.Controlled; } if (Target == character) { + // Wait character.AIController.SteeringManager.Reset(); - abandon = true; return; - } + } waitUntilPathUnreachable -= deltaTime; if (!character.IsClimbing) { @@ -78,24 +93,37 @@ namespace Barotrauma { if (e.Removed) { - abandon = true; + Abandon = true; } else { character.AIController.SelectTarget(e.AiTarget); } } - bool isInside = character.CurrentHull != null; - bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty; var targetHull = Target is Hull h ? h : Target is Item i ? i.CurrentHull : Target is Character c ? c.CurrentHull : character.CurrentHull; + if (!followControlledCharacter) + { + // Abandon if going through unsafe paths. Note ignores unsafe nodes when following an order or when the objective is set to ignore unsafe hulls. + bool containsUnsafeNodes = HumanAIController.CurrentOrder == null && !HumanAIController.ObjectiveManager.CurrentObjective.IgnoreUnsafeHulls + && PathSteering != null && PathSteering.CurrentPath != null + && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)); + if (containsUnsafeNodes || HumanAIController.UnreachableHulls.Contains(targetHull)) + { + Abandon = true; + SteeringManager.Reset(); + return; + } + } + bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty; + bool isInside = character.CurrentHull != null; bool targetIsOutside = (Target != null && targetHull == null) || (insideSteering && PathSteering.CurrentPath.HasOutdoorsNodes); if (isInside && targetIsOutside && !AllowGoingOutside) { - abandon = true; + Abandon = true; } else if (waitUntilPathUnreachable < 0) { - if (SteeringManager == PathSteering && PathSteering.CurrentPath != null && PathSteering.CurrentPath.Unreachable) + if (SteeringManager == PathSteering && PathSteering.CurrentPath != null && PathSteering.CurrentPath.Unreachable && !PathSteering.IsPathDirty) { if (repeat) { @@ -103,139 +131,147 @@ namespace Barotrauma } else { - abandon = true; + Abandon = true; } } } - if (abandon) + if (Abandon) { #if DEBUG DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target.ToString()}", Color.Yellow); #endif - if (objectiveManager.CurrentOrder != null) + if (objectiveManager.CurrentOrder != null && objectiveManager.CurrentOrder.ReportFailures) { character.Speak(TextManager.Get("DialogCannotReach"), identifier: "cannotreach", minDurationBetweenSimilar: 10.0f); } - character.AIController.SteeringManager.Reset(); + SteeringManager.Reset(); } else { - Vector2 currTargetSimPos = Vector2.Zero; - currTargetSimPos = Target.SimPosition; - // Take the sub position into account in the sim pos - if (SteeringManager != PathSteering && character.Submarine == null && Target.Submarine != null) - { - currTargetSimPos += Target.Submarine.SimPosition; - } - else if (character.Submarine != null && Target.Submarine == null) - { - currTargetSimPos -= character.Submarine.SimPosition; - } - else if (character.Submarine != Target.Submarine) - { - if (character.Submarine != null && Target.Submarine != null) - { - Vector2 diff = character.Submarine.SimPosition - Target.Submarine.SimPosition; - currTargetSimPos -= diff; - } - } - if (PathSteering != null) - { - PathSteering.startNodeFilter = startNodeFilter; - PathSteering.endNodeFilter = endNodeFilter; - } - SteeringManager.SteeringSeek(currTargetSimPos); - if (SteeringManager != PathSteering) - { - SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 1, heading: VectorExtensions.Forward(character.AnimController.Collider.Rotation)); - } - if (getDivingGearIfNeeded) + if (getDivingGearIfNeeded && !character.LockHands) { Character followTarget = Target as Character; - bool needsDivingGear = HumanAIController.NeedsDivingGear(targetHull) || mimic && HumanAIController.HasDivingMask(followTarget); - bool needsDivingSuit = needsDivingGear && (targetHull == null || targetIsOutside || targetHull.WaterPercentage > 90) || mimic && HumanAIController.HasDivingSuit(followTarget); + bool needsDivingSuit = targetIsOutside; + bool needsDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(character, targetHull, out needsDivingSuit); + if (!needsDivingGear && mimic) + { + if (HumanAIController.HasDivingSuit(followTarget)) + { + needsDivingGear = true; + needsDivingSuit = true; + } + else if (HumanAIController.HasDivingMask(followTarget)) + { + needsDivingGear = true; + } + } bool needsEquipment = false; if (needsDivingSuit) { - needsEquipment = !HumanAIController.HasDivingSuit(character); + needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.lowOxygenThreshold); } else if (needsDivingGear) { - needsEquipment = !HumanAIController.HasDivingMask(character); + needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.lowOxygenThreshold); } if (needsEquipment) { - TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager)); + TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager), + onAbandon: () => Abandon = true, + onCompleted: () => RemoveSubObjective(ref findDivingGear)); + return; } } + if (repeat && IsCloseEnough) + { + OnCompleted(); + return; + } + if (SteeringManager == PathSteering) + { + Func nodeFilter = null; + if (isInside && !AllowGoingOutside) + { + nodeFilter = node => node.Waypoint.CurrentHull != null; + } + PathSteering.SteeringSeek(character.GetRelativeSimPosition(Target), 1, startNodeFilter, endNodeFilter, nodeFilter); + } + else + { + SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Target), 10); + } + if (!insideSteering) + { + SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 1); + } } } - private bool isCompleted; - public override bool IsCompleted() + private bool IsCloseEnough { - // First check the distance - // Then the custom condition - // And finally check if can interact (heaviest) - if (isCompleted) { return true; } - if (Target == null) - { - abandon = true; - return false; - } - bool closeEnough = Vector2.DistanceSquared(Target.WorldPosition, character.WorldPosition) < CloseEnough * CloseEnough; - if (repeat) + get { + bool closeEnough = Vector2.DistanceSquared(Target.WorldPosition, character.WorldPosition) < CloseEnough * CloseEnough; if (closeEnough) { closeEnough = !(Target is Character) || Target is Character c && c.CurrentHull == character.CurrentHull; } - if (closeEnough) - { - OnCompleted(); - } + return closeEnough; + } + } + + protected override bool Check() + { + if (IsCompleted) { return true; } + // First check the distance + // Then the custom condition + // And finally check if can interact (heaviest) + if (Target == null) + { + Abandon = true; return false; } - else if (closeEnough) + if (repeat) { - if (requiredCondition == null || requiredCondition()) + return false; + } + else + { + if (IsCloseEnough) { - if (Target is Item item) + if (requiredCondition == null || requiredCondition()) { - if (character.CanInteractWith(item, out _, checkLinked: false)) { isCompleted = true; } - } - else if (Target is Character targetCharacter) - { - if (character.CanInteractWith(targetCharacter, CloseEnough)) { isCompleted = true; } - } - else - { - isCompleted = true; + if (Target is Item item) + { + if (character.CanInteractWith(item, out _, checkLinked: false)) { IsCompleted = true; } + } + else if (Target is Character targetCharacter) + { + if (character.CanInteractWith(targetCharacter, CloseEnough)) { IsCompleted = true; } + } + else + { + IsCompleted = true; + } } } } - return isCompleted; + return IsCompleted; } - public override bool IsDuplicate(AIObjective otherObjective) - { - if (!(otherObjective is AIObjectiveGoTo objective)) { return false; } - return objective.Target == Target; - } - - private void CalculateCloseEnough() - { - float interactionDistance = Target is Item i ? i.InteractDistance + Math.Max(i.Rect.Width, i.Rect.Height) / 2 : 0; - CloseEnough = Math.Max(interactionDistance, CloseEnough); - } - - protected override void OnCompleted() + private void StopMovement() { character.AIController.SteeringManager.Reset(); if (Target != null) { character.AnimController.TargetDir = Target.WorldPosition.X > character.WorldPosition.X ? Direction.Right : Direction.Left; } + } + + protected override void OnCompleted() + { + StopMovement(); + HumanAIController.FaceTarget(Target); base.OnCompleted(); } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveIdle.cs index aeabd6331..60c7f2c55 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -11,12 +11,11 @@ namespace Barotrauma { public override string DebugTag => "idle"; - const float WallAvoidDistance = 150.0f; - private readonly float newTargetIntervalMin = 5; - private readonly float newTargetIntervalMax = 15; - private readonly float standStillMin = 1; + private readonly float newTargetIntervalMin = 10; + private readonly float newTargetIntervalMax = 20; + private readonly float standStillMin = 2; private readonly float standStillMax = 10; - private readonly float walkDurationMin = 3; + private readonly float walkDurationMin = 5; private readonly float walkDurationMax = 10; private Hull currentTarget; @@ -36,7 +35,7 @@ namespace Barotrauma walkDuration = Rand.Range(0.0f, 10.0f); } - public override bool IsCompleted() => false; + protected override bool Check() => false; public override bool CanBeCompleted => true; public override bool IsLoop { get => true; set => throw new System.Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace); } @@ -93,8 +92,10 @@ namespace Barotrauma if (currentTargetIsInvalid || currentTarget == null && HumanAIController.VisibleHulls.Any(h => IsForbidden(h))) { - newTargetTimer = 0; - standStillTimer = 0; + //don't reset to zero, otherwise the character will keep calling FindTargetHulls + //almost constantly when there's a small number of potential hulls to move to + newTargetTimer = Math.Min(newTargetTimer, 0.5f); + //standStillTimer = 0.0f; } else if (character.IsClimbing) { @@ -102,7 +103,7 @@ namespace Barotrauma { newTargetTimer = 0; } - else + else if (Math.Abs(character.AnimController.TargetMovement.Y) > 0) { // Don't allow new targets when climbing. newTargetTimer = Math.Max(newTargetIntervalMin, newTargetTimer); @@ -112,7 +113,7 @@ namespace Barotrauma { if (currentTarget == null) { - newTargetTimer = 0; + newTargetTimer = Math.Min(newTargetTimer, 0.5f); } } if (newTargetTimer <= 0.0f) @@ -127,24 +128,32 @@ namespace Barotrauma else if (targetHulls.Count > 0) { //choose a random available hull - var randomHull = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced); - bool isCurrentHullOK = !HumanAIController.UnsafeHulls.Contains(character.CurrentHull) && !IsForbidden(character.CurrentHull); - if (isCurrentHullOK) + currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced); + bool isCurrentHullAllowed = !IsForbidden(character.CurrentHull); + var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, nodeFilter: node => { + if (node.Waypoint.CurrentHull == null) { return false; } // Check that there is no unsafe or forbidden hulls on the way to the target - // Only do this when the current hull is ok, because otherwise would block all paths from the current hull to the target hull. - var path = PathSteering.PathFinder.FindPath(character.SimPosition, randomHull.SimPosition); - if (path.Unreachable || path.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull) || IsForbidden(n.CurrentHull))) - { - //can't go to this room, remove it from the list and try another room next frame - int index = targetHulls.IndexOf(randomHull); - targetHulls.RemoveAt(index); - hullWeights.RemoveAt(index); - PathSteering.Reset(); - return; - } + if (node.Waypoint.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(node.Waypoint.CurrentHull)) { return false; } + if (isCurrentHullAllowed && IsForbidden(node.Waypoint.CurrentHull)) { return false; } + return true; + }); + if (path.Unreachable) + { + //can't go to this room, remove it from the list and try another room next frame + int index = targetHulls.IndexOf(currentTarget); + targetHulls.RemoveAt(index); + hullWeights.RemoveAt(index); + PathSteering.Reset(); + currentTarget = null; + return; } - currentTarget = randomHull; + searchingNewHull = false; + } + else + { + // Couldn't find a target for some reason -> reset + newTargetTimer = Math.Max(newTargetIntervalMin, newTargetTimer); searchingNewHull = false; } @@ -156,7 +165,7 @@ namespace Barotrauma bool isRoomNameFound = currentTarget.DisplayName != null; errorMsg = "(Character " + character.Name + " idling, target " + (isRoomNameFound ? currentTarget.DisplayName : currentTarget.ToString()) + ")"; #endif - var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: errorMsg); + var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: errorMsg, nodeFilter: node => node.Waypoint.CurrentHull != null); PathSteering.SetPath(path); } @@ -174,20 +183,6 @@ namespace Barotrauma if (SteeringManager != PathSteering || (PathSteering.CurrentPath != null && (PathSteering.CurrentPath.NextNode == null || PathSteering.CurrentPath.Unreachable || PathSteering.CurrentPath.HasOutdoorsNodes))) { - if (!character.AnimController.InWater) - { - standStillTimer -= deltaTime; - if (standStillTimer > 0.0f) - { - walkDuration = Rand.Range(walkDurationMin, walkDurationMax); - PathSteering.Reset(); - return; - } - if (standStillTimer < -walkDuration) - { - standStillTimer = Rand.Range(standStillMin, standStillMax); - } - } Wander(deltaTime); return; } @@ -195,64 +190,39 @@ namespace Barotrauma if (currentTarget != null) { - character.AIController.SteeringManager.SteeringSeek(currentTarget.SimPosition); + if (SteeringManager == PathSteering) + { + PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1, nodeFilter: node => node.Waypoint.CurrentHull != null); + } + else + { + character.AIController.SteeringManager.SteeringSeek(character.GetRelativeSimPosition(currentTarget)); + } + } + else + { + Wander(deltaTime); } } public void Wander(float deltaTime) { if (character.IsClimbing) { return; } - //steer away from edges of the hull - var currentHull = character.CurrentHull; - if (currentHull != null) - { - float roomWidth = currentHull.Rect.Width; - if (roomWidth < WallAvoidDistance * 4) - { - PathSteering.Reset(); - } - else - { - float leftDist = character.Position.X - currentHull.Rect.X; - float rightDist = currentHull.Rect.Right - character.Position.X; - if (leftDist < WallAvoidDistance && rightDist < WallAvoidDistance) - { - if (Math.Abs(rightDist - leftDist) > WallAvoidDistance / 2) - { - PathSteering.SteeringManual(deltaTime, Vector2.UnitX * Math.Sign(rightDist - leftDist)); - } - else - { - PathSteering.Reset(); - } - } - else if (leftDist < WallAvoidDistance) - { - float speed = (WallAvoidDistance - leftDist) / WallAvoidDistance; - PathSteering.SteeringManual(deltaTime, Vector2.UnitX * MathHelper.Clamp(speed, 0.25f, 1)); - PathSteering.WanderAngle = 0.0f; - } - else if (rightDist < WallAvoidDistance) - { - float speed = (WallAvoidDistance - rightDist) / WallAvoidDistance; - PathSteering.SteeringManual(deltaTime, -Vector2.UnitX * MathHelper.Clamp(speed, 0.25f, 1)); - PathSteering.WanderAngle = MathHelper.Pi; - } - else - { - SteeringManager.SteeringWander(); - } - } - } - else - { - SteeringManager.SteeringWander(); - } if (!character.AnimController.InWater) { - //reset vertical steering to prevent dropping down from platforms etc - character.AIController.SteeringManager.ResetY(); + standStillTimer -= deltaTime; + if (standStillTimer > 0.0f) + { + walkDuration = Rand.Range(walkDurationMin, walkDurationMax); + PathSteering.Reset(); + return; + } + if (standStillTimer < -walkDuration) + { + standStillTimer = Rand.Range(standStillMin, standStillMax); + } } + PathSteering.Wander(deltaTime); } private void FindTargetHulls() @@ -280,8 +250,10 @@ namespace Barotrauma targetHulls.Add(hull); float weight = hull.Volume; // Prefer rooms that are closer. Avoid rooms that are not in the same level. - float dist = Math.Abs(character.WorldPosition.X - hull.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y) * 5.0f; - float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 2500, dist)); + float yDist = Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y); + yDist = yDist > 100 ? yDist * 5 : 0; + float dist = Math.Abs(character.WorldPosition.X - hull.WorldPosition.X) + yDist; + float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 2500, dist)); weight *= distanceFactor; hullWeights.Add(weight); } @@ -295,10 +267,5 @@ namespace Barotrauma if (hullName == null) { return false; } return hullName.Contains("ballast") || hullName.Contains("airlock"); } - - public override bool IsDuplicate(AIObjective otherObjective) - { - return (otherObjective is AIObjectiveIdle); - } } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveLoop.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveLoop.cs index 82a276ec0..4d62d6048 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveLoop.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveLoop.cs @@ -12,7 +12,7 @@ namespace Barotrauma public Dictionary Objectives { get; private set; } = new Dictionary(); protected HashSet ignoreList = new HashSet(); private float ignoreListTimer; - private float targetUpdateTimer; + protected float targetUpdateTimer; // By default, doesn't clear the list automatically protected virtual float IgnoreListClearInterval => 0; @@ -38,8 +38,11 @@ namespace Barotrauma : base(character, objectiveManager, priorityModifier, option) { } protected override void Act(float deltaTime) { } - public override bool IsCompleted() => false; + protected override bool Check() => false; public override bool CanBeCompleted => true; + public override bool AbandonWhenCannotCompleteSubjectives => false; + public override bool AllowSubObjectiveSorting => true; + public override bool ReportFailures => false; public override bool IsLoop { get => true; set => throw new System.Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace); } @@ -69,18 +72,19 @@ namespace Barotrauma foreach (var objective in Objectives) { var target = objective.Key; - if (!objective.Value.CanBeCompleted) - { - ignoreList.Add(target); - targetUpdateTimer = 0; - } + //if (!objective.Value.CanBeCompleted && !ignoreList.Contains(target)) + //{ + // // TODO: leaks that cannot be accessed from inside cause FixLeak objective to fail, but for some reason it's not ignored. Make sure that it is. + // ignoreList.Add(target); + // targetUpdateTimer = 0; + //} if (!Targets.Contains(target)) { subObjectives.Remove(objective.Value); } } SyncRemovedObjectives(Objectives, GetList()); - if (Objectives.None() && Targets.Any()) + if (Objectives.None() && Targets.Any(t => !ignoreList.Contains(t))) { CreateObjectives(); } @@ -91,6 +95,7 @@ namespace Barotrauma public override void Reset() { + base.Reset(); ignoreList.Clear(); ignoreListTimer = 0; UpdateTargets(); @@ -98,6 +103,7 @@ namespace Barotrauma public override float GetPriority() { + if (character.LockHands) { return 0; } if (character.Submarine == null) { return 0; } if (Targets.None()) { return 0; } // Allow the target value to be more than 100. @@ -145,15 +151,26 @@ namespace Barotrauma { foreach (T target in Targets) { + if (ignoreList.Contains(target)) { continue; } if (!Objectives.TryGetValue(target, out AIObjective objective)) { objective = ObjectiveConstructor(target); - objective.Completed += () => OnObjectiveCompleted(objective, target); Objectives.Add(target, objective); if (!subObjectives.Contains(objective)) { subObjectives.Add(objective); } + objective.Completed += () => + { + Objectives.Remove(target); + OnObjectiveCompleted(objective, target); + }; + objective.Abandoned += () => + { + Objectives.Remove(target); + ignoreList.Add(target); + targetUpdateTimer = 0; + }; } } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveManager.cs index 671373dd4..012d51175 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveManager.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; using Barotrauma.Extensions; +using Microsoft.Xna.Framework; namespace Barotrauma { @@ -12,7 +13,7 @@ namespace Barotrauma public const float OrderPriority = 70; public const float RunPriority = 50; // Constantly increases the priority of the selected objective, unless overridden - public const float baseDevotion = 2; + public const float baseDevotion = 3; public List Objectives { get; private set; } = new List(); @@ -35,7 +36,13 @@ namespace Barotrauma public AIObjective CurrentOrder { get; private set; } public AIObjective CurrentObjective { get; private set; } + public bool IsCurrentOrder() where T : AIObjective => CurrentOrder is T; public bool IsCurrentObjective() where T : AIObjective => CurrentObjective is T; + public bool IsActiveObjective() where T : AIObjective => GetActiveObjective() is T; + + public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective(); + + public bool HasActiveObjective() where T : AIObjective => CurrentObjective is T || CurrentObjective != null && CurrentObjective.GetSubObjectivesRecursive().Any(so => so is T); public AIObjectiveManager(Character character) { @@ -43,7 +50,7 @@ namespace Barotrauma CreateAutonomousObjectives(); } - public void AddObjective(AIObjective objective) + public void AddObjective(T objective) where T : AIObjective { if (objective == null) { @@ -52,21 +59,32 @@ namespace Barotrauma #endif return; } - var duplicate = Objectives.Find(o => o.IsDuplicate(objective)); - if (duplicate != null) + // Can't use the generic type, because it's possible that the user of this method uses the base type AIObjective. + // We need to get the highest type. + var type = objective.GetType(); + if (objective.AllowMultipleInstances) { - duplicate.Reset(); + if (Objectives.FirstOrDefault(o => o.GetType() == type) is T existingObjective && existingObjective.IsDuplicate(objective)) + { + Objectives.Remove(existingObjective); + } } else { - Objectives.Add(objective); + Objectives.RemoveAll(o => o.GetType() == type); } + Objectives.Add(objective); } public Dictionary DelayedObjectives { get; private set; } = new Dictionary(); public void CreateAutonomousObjectives() { + foreach (var delayedObjective in DelayedObjectives) + { + CoroutineManager.StopCoroutines(delayedObjective.Value); + } + DelayedObjectives.Clear(); Objectives.Clear(); AddObjective(new AIObjectiveFindSafety(character, this)); AddObjective(new AIObjectiveIdle(character, this)); @@ -82,8 +100,8 @@ namespace Barotrauma matchingItems.RemoveAll(it => it.Submarine != character.Submarine); var item = matchingItems.GetRandom(); var order = new Order( - orderPrefab, - item ?? character.CurrentHull as Entity, + orderPrefab, + item ?? character.CurrentHull as Entity, item?.Components.FirstOrDefault(ic => ic.GetType() == orderPrefab.ItemComponentType), orderGiver: character); if (order == null) { continue; } @@ -94,15 +112,15 @@ namespace Barotrauma objectiveCount++; } } - WaitTimer = Math.Max(WaitTimer, Rand.Range(0.5f, 1f) * objectiveCount); + _waitTimer = Math.Max(_waitTimer, Rand.Range(0.5f, 1f) * objectiveCount); } - public void AddObjective(AIObjective objective, float delay, Action callback = null) + public void AddObjective(T objective, float delay, Action callback = null) where T : AIObjective { if (objective == null) { #if DEBUG - DebugConsole.ThrowError("Attempted to add a null objective to AIObjectiveManager\n" + Environment.StackTrace); + DebugConsole.ThrowError($"{character.Name}: Attempted to add a null objective to AIObjectiveManager\n" + Environment.StackTrace); #endif return; } @@ -120,14 +138,7 @@ namespace Barotrauma DelayedObjectives.Add(objective, coroutine); } - public T GetObjective() where T : AIObjective - { - foreach (AIObjective objective in Objectives) - { - if (objective is T) return (T)objective; - } - return null; - } + public T GetObjective() where T : AIObjective => Objectives.FirstOrDefault(o => o is T) as T; private AIObjective GetCurrentObjective() { @@ -143,6 +154,7 @@ namespace Barotrauma } if (previousObjective != CurrentObjective) { + previousObjective?.OnDeselected(); CurrentObjective?.OnSelected(); GetObjective().SetRandom(); } @@ -165,17 +177,17 @@ namespace Barotrauma for (int i = 0; i < Objectives.Count; i++) { var objective = Objectives[i]; - if (objective.IsCompleted()) + if (objective.IsCompleted) { #if DEBUG - DebugConsole.NewMessage($"Removing objective {objective.DebugTag}, because it is completed."); + DebugConsole.NewMessage($"{character.Name}: Removing objective {objective.DebugTag}, because it is completed.", Color.LightGreen); #endif Objectives.Remove(objective); } else if (!objective.CanBeCompleted) { #if DEBUG - DebugConsole.NewMessage($"Removing objective {objective.DebugTag}, because it cannot be completed."); + DebugConsole.NewMessage($"{character.Name}: Removing objective {objective.DebugTag}, because it cannot be completed.", Color.Red); #endif Objectives.Remove(objective); } @@ -193,7 +205,7 @@ namespace Barotrauma { Objectives.Sort((x, y) => y.GetPriority().CompareTo(x.GetPriority())); } - CurrentObjective?.SortSubObjectives(); + GetCurrentObjective()?.SortSubObjectives(); } public void DoCurrentObjective(float deltaTime) @@ -260,7 +272,10 @@ namespace Barotrauma newObjective = new AIObjectiveRescueAll(character, this, priorityModifier); break; case "repairsystems": - newObjective = new AIObjectiveRepairItems(character, this, priorityModifier) { RequireAdequateSkills = option == "jobspecific" }; + newObjective = new AIObjectiveRepairItems(character, this, priorityModifier) + { + RequireAdequateSkills = option == "jobspecific" + }; break; case "pumpwater": newObjective = new AIObjectivePumpWater(character, this, option, priorityModifier: priorityModifier); @@ -275,11 +290,21 @@ namespace Barotrauma var steering = (order?.TargetEntity as Item)?.GetComponent(); if (steering != null) steering.PosToMaintain = steering.Item.Submarine?.WorldPosition; if (order.TargetItemComponent == null) { return null; } - newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, requireEquip: false, useController: order.UseController, priorityModifier: priorityModifier) { IsLoop = true }; + newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, requireEquip: false, useController: order.UseController, priorityModifier: priorityModifier) + { + IsLoop = true, + // Don't override auto pilot unless it's an order by a player + Override = orderGiver == Character.Controlled || orderGiver.IsRemotePlayer + }; break; default: if (order.TargetItemComponent == null) { return null; } - newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, requireEquip: false, useController: order.UseController, priorityModifier: priorityModifier) { IsLoop = true }; + newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, requireEquip: false, useController: order.UseController, priorityModifier: priorityModifier) + { + IsLoop = true, + // Don't override auto control unless it's an order by a player + Override = orderGiver == Character.Controlled || orderGiver.IsRemotePlayer + }; break; } return newObjective; diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveOperateItem.cs index 3da532975..f73da37fa 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -12,17 +12,20 @@ namespace Barotrauma private ItemComponent component, controller; private Entity operateTarget; - private bool isCompleted; private bool requireEquip; private bool useController; private AIObjectiveGoTo goToObjective; private AIObjectiveGetItem getItemObjective; + public bool Override { get; set; } = true; + public override bool CanBeCompleted => base.CanBeCompleted && (!useController || controller != null); public Entity OperateTarget => operateTarget; public ItemComponent Component => component; + public Func completionCondition; + public override float GetPriority() { if (component.Item.ConditionPercentage <= 0) { return 0; } @@ -32,7 +35,7 @@ namespace Barotrauma } if (component.Item.CurrentHull == null) { return 0; } if (component.Item.CurrentHull.FireSources.Count > 0) { return 0; } - if (Character.CharacterList.Any(c => c.CurrentHull == component.Item.CurrentHull && !HumanAIController.IsFriendly(c))) { return 0; } + if (Character.CharacterList.Any(c => c.CurrentHull == component.Item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return 0; } float devotion = MathHelper.Min(10, Priority); float value = devotion + AIObjectiveManager.OrderPriority * PriorityModifier; float max = MathHelper.Min((AIObjectiveManager.OrderPriority - 1), 90); @@ -57,21 +60,27 @@ namespace Barotrauma protected override void Act(float deltaTime) { + if (character.LockHands) + { + Abandon = true; + return; + } ItemComponent target = useController ? controller : component; if (useController && controller == null) { character.Speak(TextManager.GetWithVariable("DialogCantFindController", "[item]", component.Item.Name, true), null, 2.0f, "cantfindcontroller", 30.0f); - abandon = true; + Abandon = true; return; } if (target.CanBeSelected) { if (character.CanInteractWith(target.Item, out _, checkLinked: false)) { + HumanAIController.FaceTarget(target.Item); // Don't allow to operate an item that someone already operates, unless this objective is an order - if (objectiveManager.CurrentOrder != this && Character.CharacterList.Any(c => c.SelectedConstruction == target.Item && c != character && HumanAIController.IsFriendly(c))) + if (objectiveManager.CurrentOrder != this && Character.CharacterList.Any(c => c.SelectedConstruction == target.Item && c != character && HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { - abandon = true; + // Don't abandon return; } if (character.SelectedConstruction != target.Item) @@ -80,12 +89,14 @@ namespace Barotrauma } if (component.AIOperate(deltaTime, character, this)) { - isCompleted = true; + IsCompleted = completionCondition == null || completionCondition(); } } else { - TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(target.Item, character, objectiveManager)); + TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(target.Item, character, objectiveManager, closeEnough: 50), + onAbandon: () => Abandon = true, + onCompleted: () => RemoveSubObjective(ref goToObjective)); } } else @@ -93,12 +104,14 @@ namespace Barotrauma if (component.Item.GetComponent() == null) { //controller/target can't be selected and the item cannot be picked -> objective can't be completed - abandon = true; + Abandon = true; return; } else if (!character.Inventory.Items.Contains(component.Item)) { - TryAddSubObjective(ref getItemObjective, () => new AIObjectiveGetItem(character, component.Item, objectiveManager, equip: true)); + TryAddSubObjective(ref getItemObjective, () => new AIObjectiveGetItem(character, component.Item, objectiveManager, equip: true), + onAbandon: () => Abandon = true, + onCompleted: () => RemoveSubObjective(ref getItemObjective)); } else { @@ -109,7 +122,7 @@ namespace Barotrauma if (holdable == null) { #if DEBUG - DebugConsole.ThrowError("AIObjectiveOperateItem failed - equipping item " + component.Item + " is required but the item has no Holdable component"); + DebugConsole.ThrowError($"{character.Name}: AIObjectiveOperateItem failed - equipping item " + component.Item + " is required but the item has no Holdable component"); #endif return; } @@ -139,18 +152,12 @@ namespace Barotrauma } if (component.AIOperate(deltaTime, character, this)) { - isCompleted = true; + IsCompleted = completionCondition == null || completionCondition(); } } } } - public override bool IsCompleted() => isCompleted && !IsLoop; - - public override bool IsDuplicate(AIObjective otherObjective) - { - if (!(otherObjective is AIObjectiveOperateItem operateItem)) { return false; } - return (operateItem.component == component || otherObjective.Option == Option); - } + protected override bool Check() => IsCompleted && !IsLoop; } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectivePumpWater.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectivePumpWater.cs index 7d3bd893e..7e7199068 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectivePumpWater.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectivePumpWater.cs @@ -10,13 +10,14 @@ namespace Barotrauma class AIObjectivePumpWater : AIObjectiveLoop { public override string DebugTag => "pump water"; + public override bool KeepDivingGearOn => true; + public override bool IgnoreUnsafeHulls => true; + private IEnumerable pumpList; public AIObjectivePumpWater(Character character, AIObjectiveManager objectiveManager, string option, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier, option) { } - public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectivePumpWater && otherObjective.Option == Option; - protected override void FindTargets() { if (Option == null) { return; } @@ -33,16 +34,8 @@ namespace Barotrauma if (pump.Item.ConditionPercentage <= 0) { return false; } if (pump.Item.CurrentHull.FireSources.Count > 0) { return false; } if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(pump.Item, true)) { return false; } - if (Character.CharacterList.Any(c => c.CurrentHull == pump.Item.CurrentHull && !HumanAIController.IsFriendly(c))) { return false; } - if (Option == "stoppumping") - { - if (!pump.IsActive || MathUtils.NearlyEqual(pump.FlowPercentage, 0)) { return false; } - } - else - { - if (!pump.Item.InWater) { return false; } - if (pump.IsActive && pump.FlowPercentage <= -99.9f) { return false; } - } + if (Character.CharacterList.Any(c => c.CurrentHull == pump.Item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; } + if (IsReady(pump)) { return false; } return true; } protected override IEnumerable GetList() @@ -67,8 +60,24 @@ namespace Barotrauma } } + private bool IsReady(Pump pump) + { + if (Option == "stoppumping") + { + return !pump.IsActive || MathUtils.NearlyEqual(pump.FlowPercentage, 0); + } + else + { + return !pump.Item.InWater || pump.IsActive && pump.FlowPercentage <= -99.9f; + } + } + protected override AIObjective ObjectiveConstructor(Pump pump) - => new AIObjectiveOperateItem(pump, character, objectiveManager, Option, false) { IsLoop = false }; + => new AIObjectiveOperateItem(pump, character, objectiveManager, Option, false) + { + IsLoop = false, + completionCondition = () => IsReady(pump) + }; protected override void OnObjectiveCompleted(AIObjective objective, Pump target) => HumanAIController.RemoveTargets(character, target); diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRepairItem.cs index 94f81003f..f32c44b77 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -10,6 +10,7 @@ namespace Barotrauma class AIObjectiveRepairItem : AIObjective { public override string DebugTag => "repair item"; + public override bool KeepDivingGearOn => true; public Item Item { get; private set; } @@ -18,6 +19,8 @@ namespace Barotrauma private float previousCondition = -1; private RepairTool repairTool; + private bool IsRepairing => character.SelectedConstruction == Item && Item.GetComponent()?.CurrentFixer == character; + public AIObjectiveRepairItem(Character character, Item item, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { Item = item; @@ -28,41 +31,36 @@ namespace Barotrauma // TODO: priority list? // Ignore items that are being repaired by someone else. if (Item.Repairables.Any(r => r.CurrentFixer != null && r.CurrentFixer != character)) { return 0; } - // Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally) - float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - Item.WorldPosition.Y) * 2.0f; - float distanceFactor = MathHelper.Lerp(1, 0.5f, MathUtils.InverseLerp(0, 10000, dist)); + float yDist = Math.Abs(character.WorldPosition.Y - Item.WorldPosition.Y); + yDist = yDist > 100 ? yDist * 5 : 0; + float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + yDist; + float distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 5000, dist)); + if (Item.CurrentHull == character.CurrentHull) + { + distanceFactor = 1; + } float damagePriority = MathHelper.Lerp(1, 0, Item.Condition / Item.MaxCondition); float successFactor = MathHelper.Lerp(0, 1, Item.Repairables.Average(r => r.DegreeOfSuccess(character))); - float isSelected = character.SelectedConstruction == Item ? 50 : 0; + float isSelected = IsRepairing ? 50 : 0; float devotion = (Math.Min(Priority, 10) + isSelected) / 100; float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90); - - bool isCompleted = Item.IsFullCondition; - if (isCompleted && character.SelectedConstruction == Item) - { - character?.Speak(TextManager.GetWithVariable("DialogItemRepaired", "[itemname]", Item.Name, true), null, 0.0f, "itemrepaired", 10.0f); - } - return MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + damagePriority * distanceFactor * successFactor * PriorityModifier, 0, 1)); } - public override bool IsCompleted() + protected override bool Check() { - bool isCompleted = Item.IsFullCondition; - if (isCompleted && character.SelectedConstruction == Item) + IsCompleted = Item.IsFullCondition; + if (IsCompleted && IsRepairing) { character?.Speak(TextManager.GetWithVariable("DialogItemRepaired", "[itemname]", Item.Name, true), null, 0.0f, "itemrepaired", 10.0f); } - return isCompleted; - } - - public override bool IsDuplicate(AIObjective otherObjective) - { - return otherObjective is AIObjectiveRepairItem repairObjective && repairObjective.Item == Item; + return IsCompleted; } protected override void Act(float deltaTime) { + // Only continue when the get item sub objectives have been completed. + if (subObjectives.Any()) { return; } foreach (Repairable repairable in Item.Repairables) { if (!repairable.HasRequiredItems(character, false)) @@ -72,14 +70,12 @@ namespace Barotrauma { foreach (RelatedItem requiredItem in kvp.Value) { - AddSubObjective(new AIObjectiveGetItem(character, requiredItem.Identifiers, objectiveManager, true)); + subObjectives.Add(new AIObjectiveGetItem(character, requiredItem.Identifiers, objectiveManager, true)); } } return; } } - // Only continue when the get item sub objectives have been completed. - if (subObjectives.Any()) { return; } if (repairTool == null) { FindRepairTool(); @@ -90,9 +86,9 @@ namespace Barotrauma if (containedItems == null) { #if DEBUG - DebugConsole.ThrowError("AIObjectiveRepairItem failed - the item \"" + repairTool + "\" has no proper inventory"); + DebugConsole.ThrowError($"{character.Name}: AIObjectiveRepairItem failed - the item \"" + repairTool + "\" has no proper inventory"); #endif - abandon = true; + Abandon = true; return; } // Drop empty tanks @@ -115,12 +111,15 @@ namespace Barotrauma if (fuel == null) { RemoveSubObjective(ref goToObjective); - TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, item.Identifiers, repairTool.Item.GetComponent(), objectiveManager)); + TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, item.Identifiers, repairTool.Item.GetComponent(), objectiveManager), + onCompleted: () => RemoveSubObjective(ref refuelObjective), + onAbandon: () => Abandon = true); return; } } if (character.CanInteractWith(Item, out _, checkLinked: false)) { + HumanAIController.FaceTarget(Item); if (repairTool != null) { OperateRepairTool(deltaTime); @@ -129,10 +128,10 @@ namespace Barotrauma { if (repairable.CurrentFixer != null && repairable.CurrentFixer != character) { - // Someone else is repairing the target. Abandon the objective if the other is better at this then us. - abandon = repairable.DegreeOfSuccess(character) < repairable.DegreeOfSuccess(repairable.CurrentFixer); + // Someone else is repairing the target. Abandon the objective if the other is better at this than us. + Abandon = repairable.DegreeOfSuccess(character) < repairable.DegreeOfSuccess(repairable.CurrentFixer); } - if (!abandon) + if (!Abandon) { if (character.SelectedConstruction != Item) { @@ -145,12 +144,15 @@ namespace Barotrauma else if (Item.Condition < previousCondition) { // If the current condition is less than the previous condition, we can't complete the task, so let's abandon it. The item is probably deteriorating at a greater speed than we can repair it. - abandon = true; - character?.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f); + Abandon = true; } } - if (abandon) + if (Abandon) { + if (IsRepairing) + { + character?.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f); + } repairable.StopRepairing(character); } else @@ -179,7 +181,14 @@ namespace Barotrauma } return objective; }, - onAbandon: () => character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f)); + onAbandon: () => + { + Abandon = true; + if (IsRepairing) + { + character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f); + } + }); } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRepairItems.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRepairItems.cs index 751b1271a..2939d969d 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRepairItems.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRepairItems.cs @@ -14,9 +14,12 @@ namespace Barotrauma /// public bool RequireAdequateSkills; - public AIObjectiveRepairItems(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { } + public override bool AllowMultipleInstances => true; - public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveRepairItems repairItems && repairItems.RequireAdequateSkills == RequireAdequateSkills; + public override bool IsDuplicate(T otherObjective) => + (otherObjective as AIObjective) is AIObjectiveRepairItems repairObjective && repairObjective.RequireAdequateSkills == RequireAdequateSkills; + + public AIObjectiveRepairItems(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { } protected override void CreateObjectives() { @@ -28,7 +31,21 @@ namespace Barotrauma { objective = ObjectiveConstructor(item); Objectives.Add(item, objective); - AddSubObjective(objective); + if (!subObjectives.Contains(objective)) + { + subObjectives.Add(objective); + } + objective.Completed += () => + { + Objectives.Remove(item); + OnObjectiveCompleted(objective, item); + }; + objective.Abandoned += () => + { + Objectives.Remove(item); + ignoreList.Add(item); + targetUpdateTimer = 0; + }; } break; } @@ -40,7 +57,7 @@ namespace Barotrauma if (!IsValidTarget(item, character)) { return false; } if (item.CurrentHull.FireSources.Count > 0) { return false; } // Don't repair items in rooms that have enemies inside. - if (Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !HumanAIController.IsFriendly(c))) { return false; } + if (Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; } if (!Objectives.ContainsKey(item)) { if (item.Repairables.All(r => item.ConditionPercentage > r.ShowRepairUIThreshold)) { return false; } @@ -52,7 +69,7 @@ namespace Barotrauma return true; } - protected override float TargetEvaluation() => Targets.Max(t => 100 - t.ConditionPercentage); + protected override float TargetEvaluation() => Targets.Max(t => character.SelectedConstruction == t && t.ConditionPercentage < 100 ? 100 : 100 - t.ConditionPercentage); protected override IEnumerable GetList() => Item.ItemList; protected override AIObjective ObjectiveConstructor(Item item) diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRescue.cs index dbddff518..f53eec9c0 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -10,12 +10,16 @@ namespace Barotrauma { public override string DebugTag => "rescue"; public override bool ForceRun => true; + public override bool KeepDivingGearOn => true; const float TreatmentDelay = 0.5f; + const float CloseEnoughToTreat = 150.0f; + private readonly Character targetCharacter; private AIObjectiveGoTo goToObjective; + private AIObjectiveGetItem getItemObjective; private float treatmentTimer; private Hull safeHull; @@ -24,94 +28,79 @@ namespace Barotrauma { if (targetCharacter == null) { - string errorMsg = "Attempted to create a Rescue objective with no target!\n" + Environment.StackTrace; + string errorMsg = $"{character.Name}: Attempted to create a Rescue objective with no target!\n" + Environment.StackTrace; DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("AIObjectiveRescue:ctor:targetnull", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); - abandon = true; - return; - } - - if (targetCharacter == character) - { - // TODO: enable healing self too - abandon = true; + Abandon = true; return; } this.targetCharacter = targetCharacter; } - - public override bool IsDuplicate(AIObjective otherObjective) - { - AIObjectiveRescue rescueObjective = otherObjective as AIObjectiveRescue; - return rescueObjective != null && rescueObjective.targetCharacter == targetCharacter; - } protected override void Act(float deltaTime) { - if (targetCharacter == null || targetCharacter.Removed) + if (character.LockHands || targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead) { + Abandon = true; return; } - // Unconcious target is not in a safe place -> Move to a safe place first - if (targetCharacter.IsUnconscious && HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD) + if (targetCharacter != character) { - if (character.SelectedCharacter != targetCharacter) - { - character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget", new string[2] { "[targetname]", "[roomname]" }, - new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }), - null, 1.0f, "foundunconscioustarget" + targetCharacter.Name, 60.0f); - - // Go to the target and select it - if (!character.CanInteractWith(targetCharacter)) + // Unconcious target is not in a safe place -> Move to a safe place first + if (targetCharacter.IsUnconscious && HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD) + { + if (character.SelectedCharacter != targetCharacter) { - if (goToObjective != null && goToObjective.Target != targetCharacter) + character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget", new string[2] { "[targetname]", "[roomname]" }, + new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }), + null, 1.0f, "foundunconscioustarget" + targetCharacter.Name, 60.0f); + + // Go to the target and select it + if (!character.CanInteractWith(targetCharacter)) { - goToObjective = null; + RemoveSubObjective(ref goToObjective); + TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager) { CloseEnough = CloseEnoughToTreat }, + onCompleted: () => RemoveSubObjective(ref goToObjective), + onAbandon: () => RemoveSubObjective(ref goToObjective)); + } + else + { + character.SelectCharacter(targetCharacter); } - TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager)); } else { - character.SelectCharacter(targetCharacter); - } - } - else - { - // Drag the character into safety - if (goToObjective != null && goToObjective.Target == targetCharacter) - { - goToObjective = null; - } - if (safeHull == null) - { - var findSafety = objectiveManager.GetObjective(); - if (findSafety == null) + // Drag the character into safety + if (safeHull == null) { - // Ensure that we have the find safety objective (should always be the case) - findSafety = new AIObjectiveFindSafety(character, objectiveManager); - objectiveManager.AddObjective(findSafety); + safeHull = objectiveManager.GetObjective().FindBestHull(HumanAIController.VisibleHulls); + } + if (character.CurrentHull != safeHull) + { + RemoveSubObjective(ref goToObjective); + TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(safeHull, character, objectiveManager), + onCompleted: () => RemoveSubObjective(ref goToObjective), + onAbandon: () => RemoveSubObjective(ref goToObjective)); } - safeHull = findSafety.FindBestHull(HumanAIController.VisibleHulls); - } - if (character.CurrentHull != safeHull) - { - TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(safeHull, character, objectiveManager)); } } } if (subObjectives.Any()) { return; } - if (!character.CanInteractWith(targetCharacter)) + if (targetCharacter != character && !character.CanInteractWith(targetCharacter)) { + RemoveSubObjective(ref goToObjective); // Go to the target and select it - TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager)); + TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager) { CloseEnough = CloseEnoughToTreat }, + onCompleted: () => RemoveSubObjective(ref goToObjective), + onAbandon: () => RemoveSubObjective(ref goToObjective)); } else { // We can start applying treatment - if (character.SelectedCharacter != targetCharacter) + if (character != targetCharacter && character.SelectedCharacter != targetCharacter) { character.Speak(TextManager.GetWithVariables("DialogFoundWoundedTarget", new string[2] { "[targetname]", "[roomname]" }, new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }), @@ -123,61 +112,59 @@ namespace Barotrauma } } - // TODO: consider optimizing a bit + private readonly List suitableItemIdentifiers = new List(); + private readonly List itemNameList = new List(); + private Dictionary currentTreatmentSuitabilities = new Dictionary(); private void GiveTreatment(float deltaTime) { if (treatmentTimer > 0.0f) { treatmentTimer -= deltaTime; + return; } treatmentTimer = TreatmentDelay; - var allAfflictions = targetCharacter.CharacterHealth.GetAllAfflictions() - .Where(a => a.GetVitalityDecrease(targetCharacter.CharacterHealth) > 0) - .ToList(); + //find which treatments are the most suitable to treat the character's current condition + targetCharacter.CharacterHealth.GetSuitableTreatments(currentTreatmentSuitabilities, normalize: false); - allAfflictions.Sort((a1, a2) => - { - return Math.Sign(a2.GetVitalityDecrease(targetCharacter.CharacterHealth) - a1.GetVitalityDecrease(targetCharacter.CharacterHealth)); - }); + var allAfflictions = GetVitalityReducingAfflictions(targetCharacter).OrderByDescending(a => a.GetVitalityDecrease(targetCharacter.CharacterHealth)); //check if we already have a suitable treatment for any of the afflictions foreach (Affliction affliction in allAfflictions) { foreach (KeyValuePair treatmentSuitability in affliction.Prefab.TreatmentSuitability) { - if (treatmentSuitability.Value > 0.0f) + if (currentTreatmentSuitabilities.ContainsKey(treatmentSuitability.Key) && currentTreatmentSuitabilities[treatmentSuitability.Key] > 0.0f) { - Item matchingItem = character.Inventory.FindItemByIdentifier(treatmentSuitability.Key); + Item matchingItem = character.Inventory.FindItemByIdentifier(treatmentSuitability.Key, true); if (matchingItem == null) { continue; } ApplyTreatment(affliction, matchingItem); + //wait a bit longer after applying a treatment to wait for potential side-effects to manifest + treatmentTimer = TreatmentDelay * 4; return; } } } + + float cprSuitability = targetCharacter.Oxygen < 0.0f ? -targetCharacter.Oxygen * 100.0f : 0.0f; //didn't have any suitable treatments available, try to find some medical items - HashSet suitableItemIdentifiers = new HashSet(); - foreach (Affliction affliction in allAfflictions) + if (currentTreatmentSuitabilities.Any(s => s.Value > cprSuitability)) { - foreach (KeyValuePair treatmentSuitability in affliction.Prefab.TreatmentSuitability) + itemNameList.Clear(); + suitableItemIdentifiers.Clear(); + foreach (KeyValuePair treatmentSuitability in currentTreatmentSuitabilities) { - if (treatmentSuitability.Value > 0.0f) + if (treatmentSuitability.Value <= cprSuitability) { continue; } + if (MapEntityPrefab.Find(null, treatmentSuitability.Key, showErrorMessages: false) is ItemPrefab itemPrefab) { + if (!Item.ItemList.Any(it => it.prefab.Identifier == treatmentSuitability.Key)) { continue; } suitableItemIdentifiers.Add(treatmentSuitability.Key); + //only list the first 4 items + if (itemNameList.Count < 4) + { + itemNameList.Add(itemPrefab.Name); + } } } - } - if (suitableItemIdentifiers.Count > 0) - { - List itemNameList = new List(); - foreach (string itemIdentifier in suitableItemIdentifiers) - { - if (MapEntityPrefab.Find(null, itemIdentifier, showErrorMessages: false) is ItemPrefab itemPrefab) - { - itemNameList.Add(itemPrefab.Name); - } - //only list the first 4 items - if (itemNameList.Count >= 4) { break; } - } if (itemNameList.Count > 0) { string itemListStr = ""; @@ -189,18 +176,24 @@ namespace Barotrauma { itemListStr = string.Join(" or ", string.Join(", ", itemNameList.Take(itemNameList.Count - 1)), itemNameList.Last()); } - - - character.Speak(TextManager.GetWithVariables("DialogListRequiredTreatments", new string[2] { "[targetname]", "[treatmentlist]" }, - new string[2] { targetCharacter.Name, itemListStr }, new bool[2] { false, true }), - null, 2.0f, "listrequiredtreatments" + targetCharacter.Name, 60.0f); + if (targetCharacter != character) + { + character.Speak(TextManager.GetWithVariables("DialogListRequiredTreatments", new string[2] { "[targetname]", "[treatmentlist]" }, + new string[2] { targetCharacter.Name, itemListStr }, new bool[2] { false, true }), + null, 2.0f, "listrequiredtreatments" + targetCharacter.Name, 60.0f); + } + character.DeselectCharacter(); + RemoveSubObjective(ref getItemObjective); + TryAddSubObjective(ref getItemObjective, + constructor: () => new AIObjectiveGetItem(character, suitableItemIdentifiers.ToArray(), objectiveManager, equip: true), + onCompleted: () => RemoveSubObjective(ref getItemObjective), + onAbandon: () => RemoveSubObjective(ref getItemObjective)); } - character.DeselectCharacter(); - AddSubObjective(new AIObjectiveGetItem(character, suitableItemIdentifiers.ToArray(), objectiveManager, equip: true)); } character.AnimController.Anim = AnimController.Animation.CPR; } + private void ApplyTreatment(Affliction affliction, Item item) { var targetLimb = targetCharacter.CharacterHealth.GetAfflictionLimb(affliction); @@ -224,43 +217,46 @@ namespace Barotrauma } } - public override bool IsCompleted() + protected override bool Check() { - if (targetCharacter == null || targetCharacter.Removed) + if (character.LockHands || targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead) { - abandon = true; - return true; + Abandon = true; + return false; } - - bool isCompleted = targetCharacter.Bleeding <= 0 && targetCharacter.Vitality / targetCharacter.MaxVitality > AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager); - if (isCompleted) + // Don't go into rooms that have enemies + if (Character.CharacterList.Any(c => c.CurrentHull == targetCharacter.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c))) + { + Abandon = true; + return false; + } + bool isCompleted = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) > AIObjectiveRescueAll.GetVitalityThreshold(objectiveManager); + if (isCompleted && targetCharacter != character) { character.Speak(TextManager.GetWithVariable("DialogTargetHealed", "[targetname]", targetCharacter.Name), null, 1.0f, "targethealed" + targetCharacter.Name, 60.0f); } - return isCompleted || targetCharacter.IsDead; + return isCompleted; } public override float GetPriority() { - if (targetCharacter == null) { return 0; } - if (targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead) + if (targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead) { - abandon = true; - return 0; - } - // Don't go into rooms that have enemies - if (Character.CharacterList.Any(c => c.CurrentHull == targetCharacter.CurrentHull && !HumanAIController.IsFriendly(c))) - { - abandon = true; return 0; } // Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally) float dist = Math.Abs(character.WorldPosition.X - targetCharacter.WorldPosition.X) + Math.Abs(character.WorldPosition.Y - targetCharacter.WorldPosition.Y) * 2.0f; - float distanceFactor = MathHelper.Lerp(1, 0.5f, MathUtils.InverseLerp(0, 10000, dist)); + float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, dist)); + if (targetCharacter.CurrentHull == character.CurrentHull) + { + distanceFactor = 1; + } float vitalityFactor = AIObjectiveRescueAll.GetVitalityFactor(targetCharacter); float devotion = Math.Min(Priority, 10) / 100; return MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + vitalityFactor * distanceFactor, 0, 1)); } + + public static IEnumerable GetVitalityReducingAfflictions(Character character) => character.CharacterHealth.GetAllAfflictions(a => a.GetVitalityDecrease(character.CharacterHealth) > 0); } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRescueAll.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRescueAll.cs index 12ac3a962..6cc31471c 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRescueAll.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/Objectives/AIObjectiveRescueAll.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; namespace Barotrauma @@ -7,9 +8,10 @@ namespace Barotrauma { public override string DebugTag => "rescue all"; public override bool ForceRun => true; + public override bool IgnoreUnsafeHulls => true; - private const float vitalityThreshold = 0.8f; - private const float vitalityThresholdForOrders = 0.95f; + private const float vitalityThreshold = 80; + private const float vitalityThresholdForOrders = 95; public static float GetVitalityThreshold(AIObjectiveManager manager) { if (manager == null) @@ -25,15 +27,13 @@ namespace Barotrauma public AIObjectiveRescueAll(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { } - public override bool IsDuplicate(AIObjective otherObjective) => otherObjective is AIObjectiveRescueAll; - protected override bool Filter(Character target) => IsValidTarget(target, character); protected override IEnumerable GetList() => Character.CharacterList; - protected override float TargetEvaluation() => Targets.Max(t => GetVitalityFactor(t)) * 100; + protected override float TargetEvaluation() => Targets.Max(t => GetVitalityFactor(t)); - public static float GetVitalityFactor(Character character) => (character.MaxVitality - character.Vitality) / character.MaxVitality; + public static float GetVitalityFactor(Character character) => Math.Min(character.HealthPercentage - character.Bleeding - character.Bloodloss - Math.Min(character.Oxygen, 0), 100); protected override AIObjective ObjectiveConstructor(Character target) => new AIObjectiveRescue(character, target, objectiveManager, PriorityModifier); @@ -47,16 +47,18 @@ namespace Barotrauma if (!HumanAIController.IsFriendly(character, target)) { return false; } if (character.AIController is HumanAIController humanAI) { - if (target.Bleeding < 1 && target.Vitality / target.MaxVitality > GetVitalityThreshold(humanAI.ObjectiveManager)) { return false; } + if (GetVitalityFactor(target) > GetVitalityThreshold(humanAI.ObjectiveManager)) { return false; } } else { - if (target.Bleeding < 1 && target.Vitality / target.MaxVitality > vitalityThreshold) { return false; } + if (GetVitalityFactor(target) > vitalityThreshold) { return false; } } if (target.Submarine == null || character.Submarine == null) { return false; } if (target.Submarine.TeamID != character.Submarine.TeamID) { return false; } if (target.CurrentHull == null) { return false; } if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, true)) { return false; } + // Don't go into rooms that have enemies + if (Character.CharacterList.Any(c => c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c))) { return false; } return true; } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/PathFinder.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/PathFinder.cs index a23b44fbb..f27a327e4 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/PathFinder.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/PathFinder.cs @@ -157,12 +157,13 @@ namespace Barotrauma } } - public SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub = null, string errorMsgStr = null, Func startNodeFilter = null, Func endNodeFilter = null) + public SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub = null, string errorMsgStr = null, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null) { float closestDist = 0.0f; PathNode startNode = null; foreach (PathNode node in nodes) { + if (nodeFilter != null && !nodeFilter(node)) { continue; } if (startNodeFilter != null && !startNodeFilter(node)) { continue; } Vector2 nodePos = node.Position; if (hostSub != null) @@ -220,6 +221,7 @@ namespace Barotrauma PathNode endNode = null; foreach (PathNode node in nodes) { + if (nodeFilter != null && !nodeFilter(node)) { continue; } if (endNodeFilter != null && !endNodeFilter(node)) { continue; } Vector2 nodePos = node.Position; if (hostSub != null) @@ -264,7 +266,7 @@ namespace Barotrauma return new SteeringPath(true); } - var path = FindPath(startNode, endNode); + var path = FindPath(startNode, endNode, nodeFilter); return path; } @@ -297,7 +299,7 @@ namespace Barotrauma return FindPath(startNode, endNode); } - private SteeringPath FindPath(PathNode start, PathNode end) + private SteeringPath FindPath(PathNode start, PathNode end, Func filter = null) { if (start == end) { @@ -323,7 +325,8 @@ namespace Barotrauma float dist = float.MaxValue; foreach (PathNode node in nodes) { - if (node.state != 1) continue; + if (filter != null && !filter(node)) { continue; } + if (node.state != 1) { continue; } if (node.F < dist) { dist = node.F; @@ -331,7 +334,7 @@ namespace Barotrauma } } - if (currNode == null || currNode == end) break; + if (currNode == null || currNode == end) { break; } currNode.state = 2; @@ -369,7 +372,7 @@ namespace Barotrauma if (GetNodePenalty != null) { float? nodePenalty = GetNodePenalty(currNode, nextNode); - if (nodePenalty == null) continue; + if (nodePenalty == null) { continue; } tempG += nodePenalty.Value; } @@ -388,7 +391,9 @@ namespace Barotrauma if (end.state == 0 || end.Parent == null) { - //DebugConsole.NewMessage("Pathfinding error: path not found", Color.DarkRed); +#if DEBUG + DebugConsole.NewMessage("Path not found", Color.Yellow); +#endif return new SteeringPath(true); } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/SteeringManager.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/SteeringManager.cs index 3f47266e4..290a8a3d0 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/SteeringManager.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/SteeringManager.cs @@ -15,13 +15,18 @@ namespace Barotrauma protected ISteerable host; - private Vector2 steering; - - private Vector2? avoidObstaclePos; - private float rayCastTimer; - - private float wanderAngle; + protected Vector2 steering; + private float lastRayCastTime; + + private bool avoidRayCastHit; + + public Vector2 AvoidDir { get; private set; } + public Vector2 AvoidRayCastHitPosition { get; private set; } + public Vector2 AvoidLookAheadPos { get; private set; } + + private float wanderAngle; + public float WanderAngle { get { return wanderAngle; } @@ -45,9 +50,9 @@ namespace Barotrauma steering += DoSteeringWander(weight); } - public void SteeringAvoid(float deltaTime, float lookAheadDistance, float weight = 1, Vector2? heading = null) + public void SteeringAvoid(float deltaTime, float lookAheadDistance, float weight = 1) { - steering += DoSteeringAvoid(deltaTime, lookAheadDistance, weight, heading); + steering += DoSteeringAvoid(deltaTime, lookAheadDistance, weight); } public void SteeringManual(float deltaTime, Vector2 velocity) @@ -107,7 +112,7 @@ namespace Barotrauma protected virtual Vector2 DoSteeringWander(float weight) { - Vector2 circleCenter = (host.Steering == Vector2.Zero) ? Rand.Vector(weight) : host.Steering; + Vector2 circleCenter = (host.Steering == Vector2.Zero) ? Vector2.UnitY : host.Steering; circleCenter = Vector2.Normalize(circleCenter) * CircleDistance; Vector2 displacement = new Vector2( @@ -135,70 +140,42 @@ namespace Barotrauma { return Vector2.Zero; } + float maxDistance = lookAheadDistance; - if (rayCastTimer <= 0.0f) + if (Timing.TotalTime >= lastRayCastTime + RayCastInterval) { - Vector2 ahead = host.SimPosition + Vector2.Normalize(host.Steering) * maxDistance; - rayCastTimer = RayCastInterval; - Body closestBody = Submarine.CheckVisibility(host.SimPosition, ahead); - if (closestBody == null) + avoidRayCastHit = false; + AvoidLookAheadPos = host.SimPosition + Vector2.Normalize(host.Steering) * maxDistance; + lastRayCastTime = (float)Timing.TotalTime; + Body closestBody = Submarine.CheckVisibility(host.SimPosition, AvoidLookAheadPos); + if (closestBody != null) { - avoidObstaclePos = null; - return Vector2.Zero; - } - else - { - // TODO: Doesn't take items into account (like turrets) - if (closestBody.UserData is Structure closestStructure) - { - Vector2 obstaclePosition = Submarine.LastPickedPosition; - if (closestStructure.IsHorizontal) - { - obstaclePosition.Y = closestStructure.SimPosition.Y; - } - else - { - obstaclePosition.X = closestStructure.SimPosition.X; - } - avoidObstaclePos = obstaclePosition; - } - else - { - avoidObstaclePos = Submarine.LastPickedPosition; - } + avoidRayCastHit = true; + AvoidRayCastHitPosition = Submarine.LastPickedPosition; + AvoidDir = Submarine.LastPickedNormal; + //add a bit of randomness + AvoidDir = MathUtils.RotatePoint(AvoidDir, Rand.Range(-0.15f, 0.15f)); + //wait a bit longer for the next raycast + lastRayCastTime += RayCastInterval; } } - else + + if (AvoidDir.LengthSquared() < 0.0001f) { return Vector2.Zero; } + + //if raycast hit nothing, lerp avoid dir to zero + if (!avoidRayCastHit) { - rayCastTimer -= deltaTime; + AvoidDir -= Vector2.Normalize(AvoidDir) * deltaTime * 0.5f; } - if (!avoidObstaclePos.HasValue) - { - return Vector2.Zero; - } - Vector2 diff = avoidObstaclePos.Value - host.SimPosition; + + Vector2 diff = AvoidRayCastHitPosition - host.SimPosition; float dist = diff.Length(); - if (dist > maxDistance) - { - return Vector2.Zero; - } - if (heading.HasValue) - { - var f = heading ?? host.Steering; - // Avoid to left or right depending on the current heading - Vector2 relativeVector = Vector2.Normalize(diff) - Vector2.Normalize(f); - var dir = relativeVector.X > 0 ? diff.Right() : diff.Left(); - float factor = 1.0f - Math.Min(dist / maxDistance, 1); - return dir * factor * weight; - } - else - { - // Doesn't work right because it effectively just slows down or reverses the movement, where as we'd like to go right or left to avoid the target. - // There's also another issue, which also affects going right or left: the raycast doesn't hit anything if we turn too much -> avoiding doesn't work well. - // Could probably "remember" the avoidance a bit longer so that the avoid steering is not immedieately disgarded, but kept for a while and reduced gradually? - return -diff * (1.0f - dist / maxDistance) * weight; - } + //> 0 when heading in the same direction as the obstacle, < 0 when away from it + float dot = MathHelper.Clamp(Vector2.Dot(diff / dist, host.Steering), 0.0f, 1.0f); + if (dot < 0) { return Vector2.Zero; } + + return AvoidDir * dot * weight * MathHelper.Clamp(1.0f - dist / lookAheadDistance, 0.0f, 1.0f); } } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/SwarmBehavior.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/SwarmBehavior.cs index 76adf9000..7b2abeada 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/SwarmBehavior.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/SwarmBehavior.cs @@ -10,9 +10,9 @@ namespace Barotrauma { class SwarmBehavior { - private float minDistFromClosest; - private float maxDistFromCenter; - private float cohesion; + private readonly float minDistFromClosest; + private readonly float maxDistFromCenter; + private readonly float cohesion; public List Members { get; private set; } = new List(); public HashSet ActiveMembers { get; private set; } = new HashSet(); @@ -28,23 +28,29 @@ namespace Barotrauma this.ai = ai; minDistFromClosest = ConvertUnits.ToSimUnits(element.GetAttributeFloat("mindistfromclosest", 10.0f)); maxDistFromCenter = ConvertUnits.ToSimUnits(element.GetAttributeFloat("maxdistfromcenter", 1000.0f)); - cohesion = element.GetAttributeFloat("cohesion", 0.1f); + cohesion = element.GetAttributeFloat("cohesion", 1) / 10; } public static void CreateSwarm(IEnumerable swarm) { + var aiControllers = new List(); foreach (AICharacter character in swarm) { if (character.AIController is EnemyAIController enemyAI && enemyAI.SwarmBehavior != null) { - enemyAI.SwarmBehavior.Members = swarm.ToList(); + aiControllers.Add(enemyAI); } } + var filteredMembers = aiControllers.Select(m => m.Character as AICharacter).Where(m => m != null); + foreach (EnemyAIController ai in aiControllers) + { + ai.SwarmBehavior.Members = filteredMembers.ToList(); + } } public void Refresh() { - Members.RemoveAll(m => m.IsDead || m.Removed); + Members.RemoveAll(m => m.IsDead || m.Removed || m.AIController is EnemyAIController ai && ai.State == AIState.Flee); foreach (var member in Members) { if (!member.AIController.Enabled && member.IsRemotePlayer || Character.Controlled == member || !((EnemyAIController)member.AIController).SwarmBehavior.IsActive) diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/Source/Characters/Animation/AnimController.cs index 4f3fb0a08..79fea4873 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Animation/AnimController.cs @@ -61,7 +61,7 @@ namespace Barotrauma } } - public bool CanWalk => CanEnterSubmarine; + public bool CanWalk => RagdollParams.CanWalk; public bool IsMovingBackwards => !InWater && Math.Sign(targetMovement.X) == -Math.Sign(Dir); // TODO: define death anim duration in XML diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/Source/Characters/Animation/FishAnimController.cs index 43040c7e2..b27499ef8 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Animation/FishAnimController.cs @@ -132,25 +132,33 @@ namespace Barotrauma { if (Frozen) return; if (MainLimb == null) { return; } + var mainLimb = MainLimb; levitatingCollider = true; - if (!character.AllowInput) + if (!character.CanMove) { levitatingCollider = false; Collider.FarseerBody.FixedRotation = false; if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) { Collider.Enabled = false; - Collider.FarseerBody.FixedRotation = false; Collider.LinearVelocity = MainLimb.LinearVelocity; Collider.SetTransformIgnoreContacts(MainLimb.SimPosition, MainLimb.Rotation); + //reset pull joints to prevent the character from "hanging" mid-air if pull joints had been active when the character was still moving + //(except when dragging, then we need the pull joints) + if (!character.CanBeDragged || character.SelectedBy == null) { ResetPullJoints(); } } if (character.IsDead && deathAnimTimer < deathAnimDuration) { deathAnimTimer += deltaTime; - UpdateDying(deltaTime); - } + UpdateDying(deltaTime); + } + else if (!InWater && !CanWalk && character.AllowInput) + { + //cannot walk but on dry land -> wiggle around + UpdateDying(deltaTime); + } return; } else @@ -184,20 +192,22 @@ namespace Barotrauma Collider.FarseerBody.FixedRotation = false; UpdateSineAnim(deltaTime); } - else if (CanEnterSubmarine && (currentHull != null || forceStanding) && CurrentGroundedParams != null) + else if (CanEnterSubmarine && (currentHull != null || forceStanding)) { - //rotate collider back upright - float standAngle = dir == Direction.Right ? CurrentGroundedParams.ColliderStandAngleInRadians : -CurrentGroundedParams.ColliderStandAngleInRadians; - if (Math.Abs(MathUtils.GetShortestAngle(Collider.Rotation, standAngle)) > 0.001f) + if (CurrentGroundedParams != null) { - Collider.AngularVelocity = MathUtils.GetShortestAngle(Collider.Rotation, standAngle) * 60.0f; - Collider.FarseerBody.FixedRotation = false; + //rotate collider back upright + float standAngle = dir == Direction.Right ? CurrentGroundedParams.ColliderStandAngleInRadians : -CurrentGroundedParams.ColliderStandAngleInRadians; + if (Math.Abs(MathUtils.GetShortestAngle(Collider.Rotation, standAngle)) > 0.001f) + { + Collider.AngularVelocity = MathUtils.GetShortestAngle(Collider.Rotation, standAngle) * 60.0f; + Collider.FarseerBody.FixedRotation = false; + } + else + { + Collider.FarseerBody.FixedRotation = true; + } } - else - { - Collider.FarseerBody.FixedRotation = true; - } - UpdateWalkAnim(deltaTime); } @@ -251,8 +261,9 @@ namespace Barotrauma if (character.SelectedCharacter != null) DragCharacter(character.SelectedCharacter, deltaTime); - if (!CurrentFishAnimation.Flip || IsStuck) return; - if (character.AIController != null && !character.AIController.CanFlip) return; + if (!CurrentFishAnimation.Flip) { return; } + if (IsStuck) { return; } + if (character.AIController != null && !character.AIController.CanFlip) { return; } flipCooldown -= deltaTime; @@ -376,34 +387,34 @@ namespace Barotrauma //limbs are disabled when simple physics is enabled, no need to move them if (SimplePhysicsEnabled) { return; } - - MainLimb.PullJointEnabled = true; - //MainLimb.PullJointWorldAnchorB = Collider.SimPosition; + var mainLimb = MainLimb; + mainLimb.PullJointEnabled = true; + //mainLimb.PullJointWorldAnchorB = Collider.SimPosition; if (movement.LengthSquared() < 0.00001f) { WalkPos = MathHelper.SmoothStep(WalkPos, MathHelper.PiOver2, deltaTime * 5); - MainLimb.PullJointWorldAnchorB = Collider.SimPosition; + mainLimb.PullJointWorldAnchorB = Collider.SimPosition; return; } Vector2 transformedMovement = reverse ? -movement : movement; float movementAngle = MathUtils.VectorToAngle(transformedMovement) - MathHelper.PiOver2; float mainLimbAngle = 0; - if (MainLimb.type == LimbType.Torso && TorsoAngle.HasValue) + if (mainLimb.type == LimbType.Torso && TorsoAngle.HasValue) { mainLimbAngle = TorsoAngle.Value; } - else if (MainLimb.type == LimbType.Head && HeadAngle.HasValue) + else if (mainLimb.type == LimbType.Head && HeadAngle.HasValue) { mainLimbAngle = HeadAngle.Value; } mainLimbAngle *= Dir; - while (MainLimb.Rotation - (movementAngle + mainLimbAngle) > MathHelper.Pi) + while (mainLimb.Rotation - (movementAngle + mainLimbAngle) > MathHelper.Pi) { movementAngle += MathHelper.TwoPi; } - while (MainLimb.Rotation - (movementAngle + mainLimbAngle) < -MathHelper.Pi) + while (mainLimb.Rotation - (movementAngle + mainLimbAngle) < -MathHelper.Pi) { movementAngle -= MathHelper.TwoPi; } @@ -416,7 +427,7 @@ namespace Barotrauma Limb torso = GetLimb(LimbType.Torso); if (torso != null) { - SmoothRotateWithoutWrapping(torso, movementAngle + TorsoAngle.Value * Dir, MainLimb, TorsoTorque); + SmoothRotateWithoutWrapping(torso, movementAngle + TorsoAngle.Value * Dir, mainLimb, TorsoTorque); } } if (HeadAngle.HasValue) @@ -424,7 +435,7 @@ namespace Barotrauma Limb head = GetLimb(LimbType.Head); if (head != null) { - SmoothRotateWithoutWrapping(head, movementAngle + HeadAngle.Value * Dir, MainLimb, HeadTorque); + SmoothRotateWithoutWrapping(head, movementAngle + HeadAngle.Value * Dir, mainLimb, HeadTorque); } } if (TailAngle.HasValue) @@ -432,7 +443,24 @@ namespace Barotrauma Limb tail = GetLimb(LimbType.Tail); if (tail != null) { - SmoothRotateWithoutWrapping(tail, movementAngle + TailAngle.Value * Dir, MainLimb, TailTorque); + float? mainLimbTargetAngle = null; + if (mainLimb.type == LimbType.Torso) + { + mainLimbTargetAngle = TorsoAngle; + } + else if (mainLimb.type == LimbType.Head) + { + mainLimbTargetAngle = HeadAngle; + } + float torque = TailTorque; + float maxMultiplier = CurrentSwimParams.TailTorqueMultiplier; + if (mainLimbTargetAngle.HasValue && maxMultiplier > 1) + { + float diff = Math.Abs(mainLimb.Rotation - tail.Rotation); + float offset = Math.Abs(mainLimbTargetAngle.Value - TailAngle.Value); + torque *= MathHelper.Lerp(1, maxMultiplier, MathUtils.InverseLerp(0, MathHelper.PiOver2, diff - offset)); + } + SmoothRotateWithoutWrapping(tail, movementAngle + TailAngle.Value * Dir, mainLimb, torque); } } } @@ -443,11 +471,11 @@ namespace Barotrauma { movementAngle = MathUtils.WrapAngleTwoPi(movementAngle - MathHelper.Pi); } - if (MainLimb.type == LimbType.Head && HeadAngle.HasValue) + if (mainLimb.type == LimbType.Head && HeadAngle.HasValue) { Collider.SmoothRotate(HeadAngle.Value * Dir, CurrentSwimParams.SteerTorque); } - else if (MainLimb.type == LimbType.Torso && TorsoAngle.HasValue) + else if (mainLimb.type == LimbType.Torso && TorsoAngle.HasValue) { Collider.SmoothRotate(TorsoAngle.Value * Dir, CurrentSwimParams.SteerTorque); } @@ -484,14 +512,14 @@ namespace Barotrauma case LimbType.RightFoot: if (CurrentSwimParams.FootAnglesInRadians.ContainsKey(limb.Params.ID)) { - SmoothRotateWithoutWrapping(limb, movementAngle + CurrentSwimParams.FootAnglesInRadians[limb.Params.ID] * Dir, MainLimb, FootTorque); + SmoothRotateWithoutWrapping(limb, movementAngle + CurrentSwimParams.FootAnglesInRadians[limb.Params.ID] * Dir, mainLimb, FootTorque); } break; case LimbType.Tail: if (waveLength > 0 && waveAmplitude > 0) { float waveRotation = (float)Math.Sin(WalkPos); - limb.body.ApplyTorque(waveRotation * limb.Mass * CurrentSwimParams.TailTorque * waveAmplitude); + limb.body.ApplyTorque(waveRotation * limb.Mass * waveAmplitude); } break; } @@ -499,25 +527,25 @@ namespace Barotrauma for (int i = 0; i < Limbs.Length; i++) { - if (Limbs[i].SteerForce <= 0.0f) continue; - + if (Limbs[i].SteerForce <= 0.0f) { continue; } + if (!Collider.PhysEnabled) { continue; } Vector2 pullPos = Limbs[i].PullJointWorldAnchorA; Limbs[i].body.ApplyForce(movement * Limbs[i].SteerForce * Limbs[i].Mass, pullPos); } - Vector2 mainLimbDiff = MainLimb.PullJointWorldAnchorB - MainLimb.SimPosition; + Vector2 mainLimbDiff = mainLimb.PullJointWorldAnchorB - mainLimb.SimPosition; if (CurrentSwimParams.UseSineMovement) { - MainLimb.PullJointWorldAnchorB = Vector2.SmoothStep( - MainLimb.PullJointWorldAnchorB, + mainLimb.PullJointWorldAnchorB = Vector2.SmoothStep( + mainLimb.PullJointWorldAnchorB, Collider.SimPosition, mainLimbDiff.LengthSquared() > 10.0f ? 1.0f : (float)Math.Abs(Math.Sin(WalkPos))); } else { - //MainLimb.PullJointWorldAnchorB = Collider.SimPosition; - MainLimb.PullJointWorldAnchorB = Vector2.Lerp( - MainLimb.PullJointWorldAnchorB, + //mainLimb.PullJointWorldAnchorB = Collider.SimPosition; + mainLimb.PullJointWorldAnchorB = Vector2.Lerp( + mainLimb.PullJointWorldAnchorB, Collider.SimPosition, mainLimbDiff.LengthSquared() > 10.0f ? 1.0f : 0.5f); } @@ -527,7 +555,6 @@ namespace Barotrauma void UpdateWalkAnim(float deltaTime) { - if (CurrentGroundedParams == null) { return; } movement = MathUtils.SmoothStep(movement, TargetMovement, 0.2f); Collider.LinearVelocity = new Vector2( @@ -540,12 +567,13 @@ namespace Barotrauma Vector2 colliderBottom = GetColliderBottom(); float movementAngle = 0.0f; - float mainLimbAngle = (MainLimb.type == LimbType.Torso ? TorsoAngle ?? 0 : HeadAngle ?? 0) * Dir; - while (MainLimb.Rotation - (movementAngle + mainLimbAngle) > MathHelper.Pi) + var mainLimb = MainLimb; + float mainLimbAngle = (mainLimb.type == LimbType.Torso ? TorsoAngle ?? 0 : HeadAngle ?? 0) * Dir; + while (mainLimb.Rotation - (movementAngle + mainLimbAngle) > MathHelper.Pi) { movementAngle += MathHelper.TwoPi; } - while (MainLimb.Rotation - (movementAngle + mainLimbAngle) < -MathHelper.Pi) + while (mainLimb.Rotation - (movementAngle + mainLimbAngle) < -MathHelper.Pi) { movementAngle -= MathHelper.TwoPi; } @@ -558,13 +586,13 @@ namespace Barotrauma { if (TorsoAngle.HasValue) { - SmoothRotateWithoutWrapping(torso, movementAngle + TorsoAngle.Value * Dir, MainLimb, TorsoTorque); + SmoothRotateWithoutWrapping(torso, movementAngle + TorsoAngle.Value * Dir, mainLimb, TorsoTorque); } if (TorsoPosition.HasValue) { Vector2 pos = colliderBottom + new Vector2(0, TorsoPosition.Value + stepLift); - if (torso != MainLimb) + if (torso != mainLimb) { pos.X = torso.SimPosition.X; } @@ -580,13 +608,13 @@ namespace Barotrauma { if (HeadAngle.HasValue) { - SmoothRotateWithoutWrapping(head, movementAngle + HeadAngle.Value * Dir, MainLimb, HeadTorque); + SmoothRotateWithoutWrapping(head, movementAngle + HeadAngle.Value * Dir, mainLimb, HeadTorque); } if (HeadPosition.HasValue) { Vector2 pos = colliderBottom + new Vector2(0, HeadPosition.Value + stepLift * CurrentGroundedParams.StepLiftHeadMultiplier); - if (head != MainLimb) + if (head != mainLimb) { pos.X = head.SimPosition.X; } @@ -602,12 +630,12 @@ namespace Barotrauma var tail = GetLimb(LimbType.Tail); if (tail != null) { - SmoothRotateWithoutWrapping(tail, movementAngle + TailAngle.Value * Dir, MainLimb, TailTorque); + SmoothRotateWithoutWrapping(tail, movementAngle + TailAngle.Value * Dir, mainLimb, TailTorque); } } float prevWalkPos = WalkPos; - WalkPos -= MainLimb.LinearVelocity.X * (CurrentAnimationParams.CycleSpeed / RagdollParams.JointScale / 100.0f); + WalkPos -= mainLimb.LinearVelocity.X * (CurrentAnimationParams.CycleSpeed / RagdollParams.JointScale / 100.0f); Vector2 transformedStepSize = Vector2.Zero; if (Math.Abs(TargetMovement.X) > 0.01f) @@ -673,7 +701,7 @@ namespace Barotrauma { SmoothRotateWithoutWrapping(limb, movementAngle + CurrentGroundedParams.FootAnglesInRadians[limb.Params.ID] * Dir, - MainLimb, FootTorque); + mainLimb, FootTorque); } break; case LimbType.LeftLeg: @@ -686,15 +714,16 @@ namespace Barotrauma void UpdateDying(float deltaTime) { - if (deathAnimDuration <= 0.0f) return; + if (deathAnimDuration <= 0.0f) { return; } + float noise = (PerlinNoise.GetPerlin(WalkPos * 0.002f, WalkPos * 0.003f) - 0.5f) * 5.0f; float animStrength = (1.0f - deathAnimTimer / deathAnimDuration); Limb head = GetLimb(LimbType.Head); + if (head != null && head.IsSevered) { return; } Limb tail = GetLimb(LimbType.Tail); - - if (head != null && !head.IsSevered) head.body.ApplyTorque((float)(Math.Sqrt(head.Mass) * Dir * Math.Sin(WalkPos)) * 30.0f * animStrength); - if (tail != null && !tail.IsSevered) tail.body.ApplyTorque((float)(Math.Sqrt(tail.Mass) * -Dir * Math.Sin(WalkPos)) * 30.0f * animStrength); + if (head != null && !head.IsSevered) head.body.ApplyTorque((float)(Math.Sqrt(head.Mass) * Dir * (Math.Sin(WalkPos) + noise)) * 30.0f * animStrength); + if (tail != null && !tail.IsSevered) tail.body.ApplyTorque((float)(Math.Sqrt(tail.Mass) * -Dir * (Math.Sin(WalkPos) + noise)) * 30.0f * animStrength); WalkPos += deltaTime * 10.0f * animStrength; @@ -753,12 +782,18 @@ namespace Barotrauma base.Flip(); foreach (Limb l in Limbs) { - if (!l.DoesFlip) continue; - l.body.SetTransform(l.SimPosition, -l.body.Rotation); + if (!l.DoesFlip) { continue; } + if (RagdollParams.IsSpritesheetOrientationHorizontal) + { + //horizontally aligned limbs need to be flipped 180 degrees + l.body.SetTransform(l.SimPosition, l.body.Rotation + MathHelper.Pi * Dir); + } + //no need to do anything when flipping vertically oriented limbs + //the sprite gets flipped horizontally, which does the job } } - private void Mirror() + public void Mirror(bool lerp = true) { Vector2 centerOfMass = GetCenterOfMass(); @@ -767,8 +802,20 @@ namespace Barotrauma TrySetLimbPosition(l, centerOfMass, new Vector2(centerOfMass.X - (l.SimPosition.X - centerOfMass.X), l.SimPosition.Y), - true); + lerp); l.body.PositionSmoothingFactor = 0.8f; + + if (!l.DoesFlip) { continue; } + if (RagdollParams.IsSpritesheetOrientationHorizontal) + { + //horizontally oriented sprites can be mirrored by rotating 180 deg and inverting the angle + l.body.SetTransform(l.SimPosition, -(l.body.Rotation + MathHelper.Pi)); + } + else + { + //vertically oriented limbs can be mirrored by inverting the angle (neutral angle is straight upwards) + l.body.SetTransform(l.SimPosition, -l.body.Rotation); + } } if (character.SelectedCharacter != null && CanDrag(character.SelectedCharacter)) { diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/Source/Characters/Animation/HumanoidAnimController.cs index e330c8e8d..63db09b35 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Animation/HumanoidAnimController.cs @@ -343,7 +343,7 @@ namespace Barotrauma deathAnimTimer = 0.0f; } - if (!character.AllowInput) + if (!character.CanMove) { levitatingCollider = false; Collider.FarseerBody.FixedRotation = false; @@ -352,6 +352,9 @@ namespace Barotrauma Collider.Enabled = false; Collider.LinearVelocity = MainLimb.LinearVelocity; Collider.SetTransformIgnoreContacts(MainLimb.SimPosition, MainLimb.Rotation); + //reset pull joints to prevent the character from "hanging" mid-air if pull joints had been active when the character was still moving + //(except when dragging, then we need the pull joints) + if (!character.CanBeDragged || character.SelectedBy == null) { ResetPullJoints(); } } return; } @@ -1082,9 +1085,7 @@ namespace Barotrauma Limb rightFoot = GetLimb(LimbType.RightFoot); Limb head = GetLimb(LimbType.Head); Limb torso = GetLimb(LimbType.Torso); - - Limb waist = GetLimb(LimbType.Waist); - + Limb leftHand = GetLimb(LimbType.LeftHand); Limb rightHand = GetLimb(LimbType.RightHand); @@ -1107,10 +1108,6 @@ namespace Barotrauma MoveLimb(head, new Vector2(ladderSimPos.X - 0.27f * Dir, bottomPos + WalkParams.HeadPosition), 10.5f); MoveLimb(torso, new Vector2(ladderSimPos.X - 0.27f * Dir, bottomPos + WalkParams.TorsoPosition), 10.5f); - if (waist != null) - { - //MoveLimb(waist, new Vector2(ladderSimPos.X - 0.35f * Dir, Collider.SimPosition.Y + 0.6f - ColliderHeightFromFloor), 10.5f); - } Collider.MoveToPos(new Vector2(ladderSimPos.X - 0.2f * Dir, Collider.SimPosition.Y), 10.5f); @@ -1140,42 +1137,47 @@ namespace Barotrauma Vector2 footPos = new Vector2( handPos.X - Dir * 0.05f, bottomPos + ColliderHeightFromFloor - stepHeight * 2.7f - ladderSimPos.Y); - - if (slide) - { - MoveLimb(leftFoot, new Vector2(footPos.X, footPos.Y + ladderSimPos.Y), 15.5f, true); - MoveLimb(rightFoot, new Vector2(footPos.X, footPos.Y + ladderSimPos.Y), 15.5f, true); - } - else - { - float leftFootPos = MathUtils.Round(footPos.Y + stepHeight, stepHeight * 2.0f) - stepHeight; - float prevLeftFootPos = MathUtils.Round(prevFootPos + stepHeight, stepHeight * 2.0f) - stepHeight; - MoveLimb(leftFoot, new Vector2(footPos.X, leftFootPos + ladderSimPos.Y), 15.5f, true); - float rightFootPos = MathUtils.Round(footPos.Y, stepHeight * 2.0f); - float prevRightFootPos = MathUtils.Round(prevFootPos, stepHeight * 2.0f); - MoveLimb(rightFoot, new Vector2(footPos.X, rightFootPos + ladderSimPos.Y), 15.5f, true); + //only move the feet if they're above the bottom of the ladders + //(if not, they'll just dangle in air, and the character holds itself up with it's arms) + if (footPos.Y > -ConvertUnits.ToSimUnits(character.SelectedConstruction.Rect.Height)) + { + if (slide) + { + MoveLimb(leftFoot, new Vector2(footPos.X, footPos.Y + ladderSimPos.Y), 15.5f, true); + MoveLimb(rightFoot, new Vector2(footPos.X, footPos.Y + ladderSimPos.Y), 15.5f, true); + } + else + { + float leftFootPos = MathUtils.Round(footPos.Y + stepHeight, stepHeight * 2.0f) - stepHeight; + float prevLeftFootPos = MathUtils.Round(prevFootPos + stepHeight, stepHeight * 2.0f) - stepHeight; + MoveLimb(leftFoot, new Vector2(footPos.X, leftFootPos + ladderSimPos.Y), 15.5f, true); + + float rightFootPos = MathUtils.Round(footPos.Y, stepHeight * 2.0f); + float prevRightFootPos = MathUtils.Round(prevFootPos, stepHeight * 2.0f); + MoveLimb(rightFoot, new Vector2(footPos.X, rightFootPos + ladderSimPos.Y), 15.5f, true); #if CLIENT - if (Math.Abs(leftFootPos - prevLeftFootPos) > stepHeight && leftFoot.LastImpactSoundTime < Timing.TotalTime - Limb.SoundInterval) - { - SoundPlayer.PlaySound("footstep_armor_heavy", leftFoot.WorldPosition, hullGuess: currentHull); - leftFoot.LastImpactSoundTime = (float)Timing.TotalTime; - } - if (Math.Abs(rightFootPos - prevRightFootPos) > stepHeight && rightFoot.LastImpactSoundTime < Timing.TotalTime - Limb.SoundInterval) - { - SoundPlayer.PlaySound("footstep_armor_heavy", rightFoot.WorldPosition, hullGuess: currentHull); - rightFoot.LastImpactSoundTime = (float)Timing.TotalTime; - } + if (Math.Abs(leftFootPos - prevLeftFootPos) > stepHeight && leftFoot.LastImpactSoundTime < Timing.TotalTime - Limb.SoundInterval) + { + SoundPlayer.PlaySound("footstep_armor_heavy", leftFoot.WorldPosition, hullGuess: currentHull); + leftFoot.LastImpactSoundTime = (float)Timing.TotalTime; + } + if (Math.Abs(rightFootPos - prevRightFootPos) > stepHeight && rightFoot.LastImpactSoundTime < Timing.TotalTime - Limb.SoundInterval) + { + SoundPlayer.PlaySound("footstep_armor_heavy", rightFoot.WorldPosition, hullGuess: currentHull); + rightFoot.LastImpactSoundTime = (float)Timing.TotalTime; + } #endif - prevFootPos = footPos.Y; - } + prevFootPos = footPos.Y; + } - //apply torque to the legs to make the knees bend - Limb leftLeg = GetLimb(LimbType.LeftLeg); - Limb rightLeg = GetLimb(LimbType.RightLeg); + //apply torque to the legs to make the knees bend + Limb leftLeg = GetLimb(LimbType.LeftLeg); + Limb rightLeg = GetLimb(LimbType.RightLeg); - leftLeg.body.ApplyTorque(Dir * -8.0f); - rightLeg.body.ApplyTorque(Dir * -8.0f); + leftLeg.body.ApplyTorque(Dir * -8.0f); + rightLeg.body.ApplyTorque(Dir * -8.0f); + } float movementFactor = (handPos.Y / stepHeight) * (float)Math.PI; movementFactor = 0.8f + (float)Math.Abs(Math.Sin(movementFactor)); diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/Source/Characters/Animation/Ragdoll.cs index 7332c73b2..35bd51ed3 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Animation/Ragdoll.cs @@ -174,13 +174,16 @@ namespace Barotrauma { get { - Limb torso = GetLimb(LimbType.Torso); - Limb head = GetLimb(LimbType.Head); - var mainLimb = torso ?? head; + Limb mainLimb = GetLimb(RagdollParams.MainLimb); if (mainLimb == null) { - //DebugConsole.ThrowError("No head or torso found. Using the first limb as the main limb."); - mainLimb = Limbs.FirstOrDefault(); + Limb torso = GetLimb(LimbType.Torso); + Limb head = GetLimb(LimbType.Head); + mainLimb = torso ?? head; + if (mainLimb == null) + { + mainLimb = Limbs.FirstOrDefault(); + } } return mainLimb; } @@ -261,10 +264,9 @@ namespace Barotrauma public bool CanEnterSubmarine => RagdollParams.CanEnterSubmarine; public bool CanAttackSubmarine => Limbs.Any(l => l.attack != null && l.attack.IsValidTarget(AttackTarget.Structure)); - public float Dir - { - get { return ((dir == Direction.Left) ? -1.0f : 1.0f); } - } + public float Dir => dir == Direction.Left ? -1.0f : 1.0f; + + public Direction Direction => dir; public bool InWater { @@ -883,9 +885,8 @@ namespace Barotrauma { Collider.SetTransform(ConvertUnits.ToSimUnits(intersection), Collider.Rotation); } + return; } - - return; } if (setSubmarine) @@ -1105,8 +1106,11 @@ namespace Barotrauma if (lowerHull != null) floorY = ConvertUnits.ToSimUnits(lowerHull.Rect.Y - lowerHull.Rect.Height); } } - if (HeadPosition.HasValue && - Collider.SimPosition.Y < waterSurface && waterSurface - floorY > HeadPosition * 0.95f) + float standHeight = + HeadPosition.HasValue ? HeadPosition.Value : + TorsoPosition.HasValue ? TorsoPosition.Value : + Collider.GetMaxExtent() * 0.5f; + if (Collider.SimPosition.Y < waterSurface && waterSurface - floorY > standHeight * 0.95f) { inWater = true; } @@ -1453,7 +1457,7 @@ namespace Barotrauma Vector2 rayEnd = rayStart - new Vector2(0.0f, height); - //var lowestLimb = FindLowestLimb(); + Vector2 colliderBottomDisplay = ConvertUnits.ToDisplayUnits(GetColliderBottom()); float closestFraction = 1; GameMain.World.RayCast((fixture, point, normal, fraction) => @@ -1466,6 +1470,7 @@ namespace Barotrauma break; case Physics.CollisionPlatform: Structure platform = fixture.Body.UserData as Structure; + if (colliderBottomDisplay.Y < platform.Rect.Y - 16 && (targetMovement.Y <= 0.0f || Stairs != null)) return -1; if (IgnorePlatforms && TargetMovement.Y < -0.5f || Collider.Position.Y < platform.Rect.Y) return -1; break; case Physics.CollisionWall: @@ -1568,7 +1573,7 @@ namespace Barotrauma protected void CheckDistFromCollider() { - float allowedDist = Math.Max(Math.Max(Collider.radius, Collider.width), Collider.height) * 2.0f; + float allowedDist = Math.Max(Math.Max(Collider.radius, Collider.width), Collider.height) * 2.0f; float resetDist = allowedDist * 5.0f; Vector2 diff = Collider.SimPosition - MainLimb.SimPosition; diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Attack.cs b/Barotrauma/BarotraumaShared/Source/Characters/Attack.cs index 57ecac8b2..a737636fd 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Attack.cs @@ -15,7 +15,9 @@ namespace Barotrauma { NotDefined, Water, - Ground + Ground, + Inside, + Outside } public enum AttackTarget @@ -70,7 +72,7 @@ namespace Barotrauma partial class Attack : ISerializableEntity { - [Serialize(AttackContext.NotDefined, true, description: "Is the attack used only in a specific condition?"), Editable] + [Serialize(AttackContext.NotDefined, true, description: "The attack will be used only in this context."), Editable] public AttackContext Context { get; private set; } [Serialize(AttackTarget.Any, true, description: "Does the attack target only specific targets?"), Editable] @@ -198,7 +200,7 @@ namespace Barotrauma /// public List Conditionals { get; private set; } = new List(); - private readonly List statusEffects; + private readonly List statusEffects = new List(); public void SetUser(Character user) { @@ -269,10 +271,6 @@ namespace Barotrauma switch (subElement.Name.ToString().ToLowerInvariant()) { case "statuseffect": - if (statusEffects == null) - { - statusEffects = new List(); - } statusEffects.Add(StatusEffect.Load(subElement, parentDebugName)); break; case "affliction": @@ -378,27 +376,27 @@ namespace Barotrauma { effectType = ActionType.OnEating; } - if (statusEffects == null) return attackResult; foreach (StatusEffect effect in statusEffects) { + // TODO: do we want to apply the effect at the world position or the entity positions in each cases? -> go through also other cases where status effects are applied if (effect.HasTargetType(StatusEffect.TargetType.This)) { - effect.Apply(effectType, deltaTime, attacker, attacker); + effect.Apply(effectType, deltaTime, attacker, attacker, worldPosition); } - if (target is Character) + if (targetCharacter != null) { if (effect.HasTargetType(StatusEffect.TargetType.Character)) { - effect.Apply(effectType, deltaTime, (Character)target, (Character)target); + effect.Apply(effectType, deltaTime, targetCharacter, targetCharacter); } if (effect.HasTargetType(StatusEffect.TargetType.Limb)) { - effect.Apply(effectType, deltaTime, (Character)target, attackResult.HitLimb); + effect.Apply(effectType, deltaTime, targetCharacter, attackResult.HitLimb); } if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs)) { - effect.Apply(effectType, deltaTime, (Character)target, ((Character)target).AnimController.Limbs.Cast().ToList()); + effect.Apply(effectType, deltaTime, targetCharacter, targetCharacter.AnimController.Limbs.Cast().ToList()); } } if (target is Entity entity) @@ -434,7 +432,6 @@ namespace Barotrauma var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, playSound, targetLimb); var effectType = attackResult.Damage > 0.0f ? ActionType.OnUse : ActionType.OnFailure; - if (statusEffects == null) return attackResult; foreach (StatusEffect effect in statusEffects) { @@ -507,6 +504,43 @@ namespace Barotrauma public bool IsValidContext(AttackContext context) => Context == context || Context == AttackContext.NotDefined; + public bool IsValidContext(IEnumerable contexts) + { + foreach (var context in contexts) + { + switch (context) + { + case AttackContext.Ground: + if (Context == AttackContext.Water) + { + return false; + } + break; + case AttackContext.Water: + if (Context == AttackContext.Ground) + { + return false; + } + break; + case AttackContext.Inside: + if (Context == AttackContext.Outside) + { + return false; + } + break; + case AttackContext.Outside: + if (Context == AttackContext.Inside) + { + return false; + } + break; + default: + continue; + } + } + return true; + } + public bool IsValidTarget(AttackTarget targetType) => TargetType == AttackTarget.Any || TargetType == targetType; public bool IsValidTarget(Entity target) diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Character.cs b/Barotrauma/BarotraumaShared/Source/Characters/Character.cs index 9e1e170d6..e0c689cc3 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Character.cs @@ -194,12 +194,16 @@ namespace Barotrauma } } - private string displayName; public string DisplayName { get { - return displayName != null && displayName.Length > 0 ? displayName : Name; + var displayName = Params.DisplayName; + if (string.IsNullOrWhiteSpace(displayName)) + { + displayName = TextManager.Get($"Character.{SpeciesName}", returnNull: true); + } + return displayName ?? Name; } } @@ -262,6 +266,16 @@ namespace Barotrauma get { return !IsUnconscious && Stun <= 0.0f && !IsDead; } } + public bool CanMove + { + get + { + if (!AllowInput) { return false; } + if (!AnimController.InWater && !AnimController.CanWalk) { return false; } + return true; + } + } + public bool CanInteract { get { return AllowInput && IsHumanoid && !LockHands && !Removed; } @@ -703,7 +717,6 @@ namespace Barotrauma var rootElement = doc.Root; var mainElement = rootElement.IsOverride() ? rootElement.FirstElement() : rootElement; InitProjSpecific(mainElement); - displayName = TextManager.Get($"Character.{speciesName}", true); List inventoryElements = new List(); List inventoryCommonness = new List(); @@ -750,7 +763,7 @@ namespace Barotrauma var matchingAffliction = AfflictionPrefab.List .Where(p => p.AfflictionType == "huskinfection") .Select(p => p as AfflictionPrefabHusk) - .FirstOrDefault(p => p.TargetSpecies.Contains(AfflictionHusk.GetNonHuskedSpeciesName(speciesName, p))); + .FirstOrDefault(p => p.TargetSpecies.Any(t => t.Equals(AfflictionHusk.GetNonHuskedSpeciesName(speciesName, p), StringComparison.InvariantCultureIgnoreCase))); if (matchingAffliction == null) { DebugConsole.ThrowError("Cannot find a husk infection that matches this species! Please add the speciesnames as 'targets' in the husk affliction prefab definition!"); @@ -1237,7 +1250,7 @@ namespace Barotrauma public void Control(float deltaTime, Camera cam) { ViewTarget = null; - if (!AllowInput) return; + if (!AllowInput) { return; } if (Controlled == this || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)) { @@ -1252,10 +1265,10 @@ namespace Barotrauma SmoothedCursorPosition = cursorPosition - smoothedCursorDiff; } - if (!(this is AICharacter) || Controlled == this || IsRemotePlayer) + bool playerControlled = !(this is AICharacter) || Controlled == this || IsRemotePlayer; + if (playerControlled) { Vector2 targetMovement = GetTargetMovement(); - AnimController.TargetMovement = targetMovement; AnimController.IgnorePlatforms = AnimController.TargetMovement.Y < -0.1f; } @@ -1265,7 +1278,8 @@ namespace Barotrauma ((HumanoidAnimController)AnimController).Crouching = IsKeyDown(InputType.Crouch); } - if (AnimController.onGround && + if (playerControlled && + AnimController.onGround && !AnimController.InWater && AnimController.Anim != AnimController.Animation.UsingConstruction && AnimController.Anim != AnimController.Animation.CPR && @@ -1292,13 +1306,16 @@ namespace Barotrauma { if (GameMain.NetworkMember.IsServer) { - if (dequeuedInput.HasFlag(InputNetFlags.FacingLeft)) + if (playerControlled) { - AnimController.TargetDir = Direction.Left; - } - else - { - AnimController.TargetDir = Direction.Right; + if (dequeuedInput.HasFlag(InputNetFlags.FacingLeft)) + { + AnimController.TargetDir = Direction.Left; + } + else + { + AnimController.TargetDir = Direction.Right; + } } } else if (GameMain.NetworkMember.IsClient && Controlled != this) @@ -1327,8 +1344,8 @@ namespace Barotrauma } else if (IsKeyDown(InputType.Attack)) { - AttackContext currentContext = GetAttackContext(); - var validLimbs = AnimController.Limbs.Where(l => !l.IsSevered && !l.IsStuck && l.attack != null && l.attack.IsValidContext(currentContext)); + var currentContexts = GetAttackContexts(); + var validLimbs = AnimController.Limbs.Where(l => !l.IsSevered && !l.IsStuck && l.attack != null && l.attack.IsValidContext(currentContexts)); var sortedLimbs = validLimbs.OrderBy(l => Vector2.DistanceSquared(ConvertUnits.ToDisplayUnits(l.SimPosition), cursorPosition)); // Select closest var attackLimb = sortedLimbs.FirstOrDefault(); @@ -1457,14 +1474,7 @@ namespace Barotrauma public bool CanSeeCharacter(Character target) { Limb seeingLimb = GetSeeingLimb(); - foreach (var targetLimb in target.AnimController.Limbs) - { - if (CanSeeTarget(targetLimb, seeingLimb)) - { - return true; - } - } - return false; + return target.AnimController.Limbs.Any(l => CanSeeTarget(l, seeingLimb)); } private Limb GetSeeingLimb() @@ -1546,8 +1556,7 @@ namespace Barotrauma return (wall == null || !wall.CastShadow) && (door == null || door.IsOpen); } - public bool HasItem(Item item, bool requireEquipped = false) => - requireEquipped ? HasEquippedItem(item) : item.FindParentInventory(i => i.Owner == this) != null; + public bool HasItem(Item item, bool requireEquipped = false) => requireEquipped ? HasEquippedItem(item) : item.IsOwnedBy(this); public bool HasEquippedItem(Item item) { @@ -1636,6 +1645,62 @@ namespace Barotrauma return true; } + private float _selectedItemPriority; + private Item _foundItem; + /// + /// Finds the closest item seeking by identifiers or tags from the world. + /// Ignores items that are outside or in another team's submarine or in a submarine that is not connected to this submarine. + /// Also ignores items that are taken by someone else. + /// The method is run in steps for performance reasons. So you'll have to provide the reference to the itemIndex. + /// Returns false while running and true when done. + /// + public bool FindItem(ref int itemIndex, out Item targetItem, IEnumerable identifiers = null, bool ignoreBroken = true, + IEnumerable ignoredItems = null, IEnumerable ignoredContainerIdentifiers = null, + Func customPredicate = null, Func customPriorityFunction = null, float maxItemDistance = 10000) + { + if (itemIndex == 0) + { + _foundItem = null; + _selectedItemPriority = 0; + } + for (int i = 0; i < 10 && itemIndex < Item.ItemList.Count - 1; i++) + { + itemIndex++; + var item = Item.ItemList[itemIndex]; + if (ignoredItems != null && ignoredItems.Contains(item)) { continue; } + if (item.Submarine == null) { continue; } + if (item.Submarine.TeamID != TeamID) { continue; } + if (Submarine != null && !Submarine.IsEntityFoundOnThisSub(item, true)) { continue; } + if (item.CurrentHull == null) { continue; } + if (ignoreBroken && item.Condition <= 0) { continue; } + if (customPredicate != null && !customPredicate(item)) { continue; } + if (identifiers != null && identifiers.None(id => item.Prefab.Identifier == id || item.HasTag(id))) { continue; } + if (ignoredContainerIdentifiers != null && item.Container != null) + { + if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; } + } + if (IsItemTakenBySomeoneElse(item)) { continue; } + float itemPriority = customPriorityFunction != null ? customPriorityFunction(item) : 1; + if (itemPriority <= 0) { continue; } + Item rootContainer = item.GetRootContainer(); + Vector2 itemPos = (rootContainer ?? item).WorldPosition; + float yDist = Math.Abs(WorldPosition.Y - itemPos.Y); + yDist = yDist > 100 ? yDist * 5 : 0; + float dist = Math.Abs(WorldPosition.X - itemPos.X) + yDist; + float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, maxItemDistance, dist)); + itemPriority *= distanceFactor; + if (itemPriority > _selectedItemPriority) + { + _selectedItemPriority = itemPriority; + _foundItem = item; + } + } + targetItem = _foundItem; + return itemIndex >= Item.ItemList.Count - 1; + } + + public bool IsItemTakenBySomeoneElse(Item item) => item.FindParentInventory(i => i.Owner != this && i.Owner is Character owner && !owner.IsDead && !owner.Removed) != null; + public bool CanInteractWith(Character c, float maxDist = 200.0f, bool checkVisibility = true) { if (c == this || Removed || !c.Enabled || !c.CanBeSelected) return false; @@ -1949,7 +2014,7 @@ namespace Barotrauma } #endif } - else if (IsKeyHit(InputType.Deselect) && SelectedConstruction != null) + else if (IsKeyHit(InputType.Deselect) && SelectedConstruction != null && SelectedConstruction.GetComponent() == null) { SelectedConstruction = null; #if CLIENT @@ -2516,16 +2581,19 @@ namespace Barotrauma //character inside the sub received damage from a monster outside the sub //can happen during normal gameplay if someone for example fires a ranged weapon from outside, //the intention of this error message is to diagnose an issue with monsters being able to damage characters from outside - if (attacker?.AIController is EnemyAIController && Submarine != null && attacker.Submarine == null) - { - string errorMsg = $"Character {Name} received damage from outside the sub while inside (attacker: {attacker.Name})"; - GameAnalyticsManager.AddErrorEventOnce("Character.DamageLimb:DamageFromOutside" + Name + attacker.Name, - GameAnalyticsSDK.Net.EGAErrorSeverity.Warning, - errorMsg + "\n" + Environment.StackTrace); -#if DEBUG - DebugConsole.ThrowError(errorMsg); -#endif - } + + // Disabled, because this happens every now and then when the monsters can get in and out of the sub. + +// if (attacker?.AIController is EnemyAIController && Submarine != null && attacker.Submarine == null) +// { +// string errorMsg = $"Character {Name} received damage from outside the sub while inside (attacker: {attacker.Name})"; +// GameAnalyticsManager.AddErrorEventOnce("Character.DamageLimb:DamageFromOutside" + Name + attacker.Name, +// GameAnalyticsSDK.Net.EGAErrorSeverity.Warning, +// errorMsg + "\n" + Environment.StackTrace); +//#if DEBUG +// DebugConsole.ThrowError(errorMsg); +//#endif +// } if (attacker != null && attacker != this && GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.AllowFriendlyFire) { @@ -2660,6 +2728,8 @@ namespace Barotrauma return; } + IsDead = true; + ApplyStatusEffects(ActionType.OnDeath, 1.0f); AnimController.Frozen = false; @@ -2691,8 +2761,6 @@ namespace Barotrauma KillProjSpecific(causeOfDeath, causeOfDeathAffliction); - IsDead = true; - if (info != null) info.CauseOfDeath = CauseOfDeath; AnimController.movement = Vector2.Zero; AnimController.TargetMovement = Vector2.Zero; @@ -2841,7 +2909,29 @@ namespace Barotrauma } } - public AttackContext GetAttackContext() => AnimController.CurrentAnimationParams.IsGroundedAnimation ? AttackContext.Ground : AttackContext.Water; + private HashSet currentContexts = new HashSet(); + + public IEnumerable GetAttackContexts() + { + currentContexts.Clear(); + if (AnimController.CurrentAnimationParams.IsGroundedAnimation) + { + currentContexts.Add(AttackContext.Ground); + } + else + { + currentContexts.Add(AttackContext.Water); + } + if (CurrentHull == null) + { + currentContexts.Add(AttackContext.Outside); + } + else + { + currentContexts.Add(AttackContext.Inside); + } + return currentContexts; + } private readonly List visibleHulls = new List(); private readonly HashSet tempList = new HashSet(); @@ -2869,7 +2959,7 @@ namespace Barotrauma } } } - visibleHulls.AddRange(CurrentHull.GetLinkedEntities(tempList, filter: h => + visibleHulls.AddRange(CurrentHull.GetLinkedEntities(tempList, filter: h => { // Ignore adjacent hulls because they were already handled above if (adjacentHulls.Contains(h)) @@ -2894,5 +2984,43 @@ namespace Barotrauma } return visibleHulls; } + + public Vector2 GetRelativeSimPosition(ISpatialEntity target, Vector2? worldPos = null) + { + Vector2 targetPos = target.SimPosition; + if (worldPos.HasValue) + { + Vector2 wp = worldPos.Value; + if (target.Submarine != null) + { + wp -= target.Submarine.Position; + } + targetPos = ConvertUnits.ToSimUnits(wp); + } + if (Submarine == null && target.Submarine != null) + { + if (AIController == null || !(AIController.SteeringManager is IndoorsSteeringManager)) + { + // outside and targeting inside + // doesn't work with inside steering + targetPos += target.Submarine.SimPosition; + } + } + else if (Submarine != null && target.Submarine == null) + { + // inside and targeting outside + targetPos -= Submarine.SimPosition; + } + else if (Submarine != target.Submarine) + { + if (Submarine != null && target.Submarine != null) + { + // both inside, but in different subs + Vector2 diff = Submarine.SimPosition - target.Submarine.SimPosition; + targetPos -= diff; + } + } + return targetPos; + } } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/Source/Characters/CharacterInfo.cs index 536efd748..805ecaa65 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/CharacterInfo.cs @@ -11,7 +11,7 @@ using System.Xml.Linq; namespace Barotrauma { public enum Gender { None, Male, Female }; - public enum Race { None, White, Black, Asian }; + public enum Race { None, White, Black, Brown, Asian }; // TODO: Generating the HeadInfo could be simplified. partial class CharacterInfo @@ -33,8 +33,10 @@ namespace Barotrauma { _headSpriteId = (int)headSpriteRange.X; } + GetSpriteSheetIndex(); } } + public Vector2? SheetIndex { get; private set; } public Vector2 headSpriteRange; public Gender gender; public Race race; @@ -51,9 +53,16 @@ namespace Barotrauma public HeadInfo() { } - public HeadInfo(int headId) + public HeadInfo(int headId, Gender gender, Race race, int hairIndex = 0, int beardIndex = 0, int moustacheIndex = 0, int faceAttachmentIndex = 0) { _headSpriteId = Math.Max(headId, 1); + this.gender = gender; + this.race = race; + HairIndex = hairIndex; + BeardIndex = beardIndex; + MoustacheIndex = moustacheIndex; + FaceAttachmentIndex = faceAttachmentIndex; + GetSpriteSheetIndex(); } public void ResetAttachmentIndices() @@ -63,6 +72,21 @@ namespace Barotrauma MoustacheIndex = -1; FaceAttachmentIndex = -1; } + + private void GetSpriteSheetIndex() + { + if (heads != null && heads.Any()) + { + var matchingHead = heads.Keys.FirstOrDefault(h => h.Gender == gender && h.Race == race && h.ID == _headSpriteId); + if (matchingHead != null) + { + if (heads.TryGetValue(matchingHead, out Vector2 index)) + { + SheetIndex = index; + } + } + } + } } private HeadInfo head; @@ -86,6 +110,43 @@ namespace Barotrauma } } + public Dictionary Heads + { + get + { + if (heads == null) + { + LoadHeadPresets(); + } + return heads; + } + } + + private static Dictionary heads; + public class HeadPreset : ISerializableEntity + { + [Serialize(Race.None, false)] + public Race Race { get; private set; } + + [Serialize(Gender.None, false)] + public Gender Gender { get; private set; } + + [Serialize(0, false)] + public int ID { get; private set; } + + [Serialize("0,0", false)] + public Vector2 SheetIndex { get; private set; } + + public string Name => $"Head Preset {Race} {Gender} {ID}"; + + public Dictionary SerializableProperties { get; private set; } + + public HeadPreset(XElement element) + { + SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + } + } + private static ushort idCounter; public string Name; @@ -158,6 +219,12 @@ namespace Barotrauma { LoadHeadSprite(); } +#if CLIENT + if (headSprite != null) + { + CalculateHeadPosition(headSprite); + } +#endif return headSprite; } private set @@ -170,6 +237,8 @@ namespace Barotrauma } } + public bool OmitJobInPortraitClothing; + private Sprite portrait; public Sprite Portrait { @@ -223,7 +292,7 @@ namespace Barotrauma { if (attachmentSprites == null) { - LoadAttachmentSprites(); + LoadAttachmentSprites(OmitJobInPortraitClothing); } return attachmentSprites; } @@ -350,7 +419,7 @@ namespace Barotrauma public bool IsAttachmentsLoaded => HairIndex > -1 && BeardIndex > -1 && MoustacheIndex > -1 && FaceAttachmentIndex > -1; // Used for creating the data - public CharacterInfo(string speciesName, string name = "", JobPrefab jobPrefab = null, string ragdollFileName = null) + public CharacterInfo(string speciesName, string name = "", JobPrefab jobPrefab = null, string ragdollFileName = null, int variant = 0) { if (speciesName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase)) { @@ -372,7 +441,7 @@ namespace Barotrauma Head.race = GetRandomRace(); CalculateHeadSpriteRange(); Head.HeadSpriteId = GetRandomHeadID(); - Job = (jobPrefab == null) ? Job.Random(Rand.RandSync.Server) : new Job(jobPrefab); + Job = (jobPrefab == null) ? Job.Random(Rand.RandSync.Server) : new Job(jobPrefab, variant); if (!string.IsNullOrEmpty(name)) { Name = name; @@ -534,11 +603,38 @@ namespace Barotrauma Enum.TryParse(w.GetAttributeString("race", "None"), true, out Race r) && r == Head.race); } + private void LoadHeadPresets() + { + if (CharacterConfigElement == null) { return; } + heads = new Dictionary(); + var headsElement = CharacterConfigElement.GetChildElement("heads"); + if (headsElement != null) + { + foreach (var head in headsElement.GetChildElements("head")) + { + var preset = new HeadPreset(head); + heads.Add(preset, preset.SheetIndex); + } + } + } + private void CalculateHeadSpriteRange() { if (CharacterConfigElement == null) { return; } Head.headSpriteRange = CharacterConfigElement.GetAttributeVector2("headidrange", Vector2.Zero); - // If range is defined, we use it as it is + // If the range is defined, we use it as it is + if (Head.headSpriteRange != Vector2.Zero) { return; } + if (heads == null) + { + LoadHeadPresets(); + } + // If there are any head presets defined, use them. + if (heads.Any()) + { + var ids = heads.Keys.Where(h => h.Race == Race && h.Gender == Gender).Select(w => w.ID); + ids = ids.OrderBy(id => id); + Head.headSpriteRange = new Vector2(ids.First(), ids.Last()); + } // Else we calculate the range from the wearables. if (Head.headSpriteRange == Vector2.Zero) { @@ -580,23 +676,13 @@ namespace Barotrauma { gender = Gender.None; } - - head = new HeadInfo(headID) - { - race = race, - gender = gender, - HairIndex = hairIndex, - BeardIndex = beardIndex, - MoustacheIndex = moustacheIndex, - FaceAttachmentIndex = faceAttachmentIndex - }; + head = new HeadInfo(headID, gender, race, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); CalculateHeadSpriteRange(); ReloadHeadAttachments(); } public void LoadHeadSprite() { - // TODO: use ragdollparams instead? foreach (XElement limbElement in Ragdoll.MainElement.Elements()) { if (limbElement.GetAttributeString("type", "").ToLowerInvariant() != "head") { continue; } @@ -649,7 +735,8 @@ namespace Barotrauma { if (hairs == null) { - hairs = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables), WearableType.Hair), WearableType.Hair); + float commonness = Gender == Gender.Female ? 0.05f : 0.2f; + hairs = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables), WearableType.Hair), WearableType.Hair, commonness); } if (beards == null) { @@ -701,10 +788,10 @@ namespace Barotrauma Head.FaceAttachmentIndex = faceAttachments.IndexOf(Head.FaceAttachment); } - List AddEmpty(IEnumerable elements, WearableType type) + List AddEmpty(IEnumerable elements, WearableType type, float commonness = 1) { // Let's add an empty element so that there's a chance that we don't get any actual element -> allows bald and beardless guys, for example. - var emptyElement = new XElement("EmptyWearable", type.ToString()); + var emptyElement = new XElement("EmptyWearable", type.ToString(), new XAttribute("commonness", commonness)); var list = new List() { emptyElement }; list.AddRange(elements); return list; @@ -743,7 +830,7 @@ namespace Barotrauma } } - partial void LoadAttachmentSprites(); + partial void LoadAttachmentSprites(bool omitJob); // TODO: change the formula so that it's not linear and so that it takes into account the usefulness of the skill // -> give a weight to each skill, because some are much more valuable than others? diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/Source/Characters/Health/Afflictions/AfflictionPrefab.cs index f1a1a8ee1..78be69b4d 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -243,7 +243,8 @@ namespace Barotrauma XElement sourceElement = isOverride ? element.FirstElement() : element; string elementName = sourceElement.Name.ToString().ToLowerInvariant(); string identifier = sourceElement.GetAttributeString("identifier", null); - if (!elementName.Equals("cprsettings", StringComparison.OrdinalIgnoreCase)) + if (!elementName.Equals("cprsettings", StringComparison.OrdinalIgnoreCase) && + !elementName.Equals("damageoverlay", StringComparison.OrdinalIgnoreCase)) { if (string.IsNullOrWhiteSpace(identifier)) { @@ -265,16 +266,38 @@ namespace Barotrauma } } } - string type = sourceElement.GetAttributeString("type", null); - if (sourceElement.Name.ToString().ToLowerInvariant() == "cprsettings") + string type = sourceElement.GetAttributeString("type", ""); + switch (sourceElement.Name.ToString().ToLowerInvariant()) { - //backwards compatibility - type = "cprsettings"; + case "cprsettings": + type = "cprsettings"; + break; + case "damageoverlay": + type = "damageoverlay"; + break; } AfflictionPrefab prefab = null; switch (type) { + case "damageoverlay": +#if CLIENT + if (CharacterHealth.DamageOverlay != null) + { + if (isOverride) + { + DebugConsole.NewMessage($"Overriding damage overlay with '{filePath}'", Color.Yellow); + } + else + { + DebugConsole.ThrowError($"Error in '{filePath}': damage overlay already loaded. Add tags as the parent of the custom damage overlay sprite to allow overriding the vanilla one."); + break; + } + } + CharacterHealth.DamageOverlay?.Remove(); + CharacterHealth.DamageOverlay = new Sprite(element); +#endif + break; case "bleeding": prefab = new AfflictionPrefab(sourceElement, typeof(AfflictionBleeding)); break; diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/Source/Characters/Health/CharacterHealth.cs index ee7a53b8b..b0645e79c 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Health/CharacterHealth.cs @@ -781,6 +781,54 @@ namespace Barotrauma return allAfflictions; } + /// + /// Get the identifiers of the items that can be used to treat the character. Takes into account all the afflictions the character has, + /// and negative treatment suitabilities (e.g. a medicine that causes oxygen loss may not be suitable if the character is already suffocating) + /// + /// A dictionary where the key is the identifier of the item and the value the suitability + /// If true, the suitability values are normalized between 0 and 1. If not, they're arbitrary values defined in the medical item XML, where negative values are unsuitable, and positive ones suitable. + /// Amount of randomization to apply to the values (0 = the values are accurate, 1 = the values are completely random) + + public void GetSuitableTreatments(Dictionary treatmentSuitability, bool normalize, float randomization = 0.0f) + { + //key = item identifier + //float = suitability + treatmentSuitability.Clear(); + float minSuitability = -10, maxSuitability = 10; + foreach (Affliction affliction in GetAllAfflictions()) + { + 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 + if (normalize) + { + foreach (string treatment in treatmentSuitability.Keys.ToList()) + { + treatmentSuitability[treatment] = (treatmentSuitability[treatment] - minSuitability) / (maxSuitability - minSuitability); + treatmentSuitability[treatment] = MathHelper.Lerp(treatmentSuitability[treatment], Rand.Range(0.0f, 1.0f), randomization); + } + } + else + { + foreach (string treatment in treatmentSuitability.Keys.ToList()) + { + treatmentSuitability[treatment] += Rand.Range(-100.0f, 100.0f) * randomization; + } + } + } + public void ServerWrite(IWriteMessage msg) { List activeAfflictions = afflictions.FindAll(a => a.Strength > 0.0f && a.Strength >= a.Prefab.ActivationThreshold); diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Jobs/Job.cs b/Barotrauma/BarotraumaShared/Source/Characters/Jobs/Job.cs index 979f97eb7..8e89b5db4 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Jobs/Job.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Jobs/Job.cs @@ -36,9 +36,12 @@ namespace Barotrauma get { return skills.Values.ToList(); } } - public Job(JobPrefab jobPrefab) + public int Variant; + + public Job(JobPrefab jobPrefab, int variant = 0) { prefab = jobPrefab; + Variant = variant; skills = new Dictionary(); foreach (SkillPrefab skillPrefab in prefab.Skills) @@ -156,6 +159,25 @@ namespace Barotrauma character.Inventory.TryPutItem(item, null, item.AllowedSlots); } + Wearable wearable = ((List)item.Components)?.Find(c => c is Wearable) as Wearable; + if (wearable != null) + { + if (Variant > 0 && Variant <= wearable.Variants) + { + wearable.Variant = Variant; + } + else + { + wearable.Variant = wearable.Variant; //force server event + if (wearable.Variants > 0 && Variant == 0) + { + //set variant to the same as the wearable to get the rest of the character's gear + //to use the same variant (if possible) + Variant = wearable.Variant; + } + } + } + if (item.Prefab.Identifier == "idcard" && spawnPoint != null) { foreach (string s in spawnPoint.IdCardTags) diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaShared/Source/Characters/Jobs/JobPrefab.cs index cc09ccc9f..bc3567fd7 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Jobs/JobPrefab.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Xml.Linq; using Barotrauma.Extensions; using System.Linq; +using System.IO; namespace Barotrauma { @@ -31,6 +32,8 @@ namespace Barotrauma partial class JobPrefab { public static Dictionary List; + + public static XElement NoJobElement; public static JobPrefab Get(string identifier) { if (List == null) @@ -146,8 +149,11 @@ namespace Barotrauma private set; } + public XElement Element { get; private set; } public XElement ClothingElement { get; private set; } + public XElement PreviewElement { get; private set; } + public JobPrefab(XElement element) { SerializableProperty.DeserializeProperties(this, element); @@ -155,6 +161,8 @@ namespace Barotrauma Description = TextManager.Get("JobDescription." + Identifier); Identifier = Identifier.ToLowerInvariant(); + Element = element; + foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -220,7 +228,71 @@ namespace Barotrauma { ClothingElement = element.Element("portraitclothing"); } + + PreviewElement = element.Element("PreviewSprites"); + if (PreviewElement == null) + { + PreviewElement = element.Element("previewsprites"); + } } + + public class OutfitPreview + { + /// + /// Pair.First = sprite, Pair.Second = draw offset + /// + public readonly List> Sprites; + + public OutfitPreview() + { + Sprites = new List>(); + } + + public void AddSprite(Sprite sprite, Vector2 drawOffset) + { + Sprites.Add(new Pair(sprite, drawOffset)); + } + } + + public List GetJobOutfitSprites(Gender gender, out Vector2 dimensions) + { + List outfitPreviews = new List(); + dimensions = PreviewElement.GetAttributeVector2("dims", Vector2.One); + if (PreviewElement == null) { return outfitPreviews; } + + var equipIdentifiers = Element.Elements("Items").Elements().Where(e => e.GetAttributeBool("outfit", false)).Select(e => e.GetAttributeString("identifier", "")); + + var children = PreviewElement.Elements().ToList(); + + var outfitPrefab = MapEntityPrefab.List.Find(me => me is ItemPrefab itemPrefab && equipIdentifiers.Contains(itemPrefab.Identifier)) as ItemPrefab; + if (outfitPrefab == null) { return null; } + var wearables = outfitPrefab.ConfigElement.Elements("Wearable"); + if (!wearables.Any()) { return null; } + + int variantCount = wearables.First().GetAttributeInt("variants", 1); + + for (int i = 0; i < variantCount; i++) + { + var outfitPreview = new OutfitPreview(); + for (int n = 0; n < children.Count; n++) + { + XElement spriteElement = children[n]; + string spriteTexture = spriteElement.GetAttributeString("texture", "").Replace("[GENDER]", (gender == Gender.Female) ? "female" : "male"); + string textureVariant = spriteTexture.Replace("[VARIANT]", (i + 1).ToString()); + if (!File.Exists(textureVariant)) + { + textureVariant = spriteTexture.Replace("[VARIANT]", "1"); + } + var torsoSprite = new Sprite(spriteElement, path: "", file: textureVariant); + torsoSprite.size = new Vector2(torsoSprite.SourceRect.Width, torsoSprite.SourceRect.Height); + outfitPreview.AddSprite(torsoSprite, children[n].GetAttributeVector2("offset", Vector2.Zero)); + } + outfitPreviews.Add(outfitPreview); + } + + return outfitPreviews; + } + public static JobPrefab Random(Rand.RandSync sync = Rand.RandSync.Unsynced) => List.Values.GetRandom(sync); @@ -239,6 +311,7 @@ namespace Barotrauma } foreach (XElement element in mainElement.Elements()) { + if (element.Name.ToString().ToLowerInvariant() == "nojob") { continue; } if (element.IsOverride()) { var job = new JobPrefab(element.FirstElement()); @@ -262,6 +335,8 @@ namespace Barotrauma } } } + NoJobElement = NoJobElement ?? mainElement.Element("NoJob"); + NoJobElement = NoJobElement ?? mainElement.Element("nojob"); } } } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Limb.cs b/Barotrauma/BarotraumaShared/Source/Characters/Limb.cs index c394e5e6a..7da89ed72 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Limb.cs @@ -435,19 +435,12 @@ namespace Barotrauma //sector 360 degrees or more -> always hits if (Math.Abs(armorSector.Y - armorSector.X) >= MathHelper.TwoPi) { return true; } float rotation = body.TransformedRotation; - float offset = (MathHelper.PiOver2 - GetArmorSectorRotationOffset(armorSector)) * Dir; + float offset = (MathHelper.PiOver2 - MathUtils.GetMidAngle(armorSector.X, armorSector.Y)) * Dir; float hitAngle = VectorExtensions.Angle(VectorExtensions.Forward(rotation + offset), SimPosition - simPosition); float sectorSize = GetArmorSectorSize(armorSector); return hitAngle < sectorSize / 2; } - protected float GetArmorSectorRotationOffset(Vector2 armorSector) - { - float midAngle = MathUtils.GetMidAngle(armorSector.X, armorSector.Y); - float spritesheetOrientation = Params.GetSpriteOrientation(); - return midAngle + spritesheetOrientation; - } - protected float GetArmorSectorSize(Vector2 armorSector) { return Math.Abs(armorSector.X - armorSector.Y); diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Params/Animation/FishAnimations.cs b/Barotrauma/BarotraumaShared/Source/Characters/Params/Animation/FishAnimations.cs index 176312c13..0d9f8a24e 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Params/Animation/FishAnimations.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Params/Animation/FishAnimations.cs @@ -140,7 +140,7 @@ namespace Barotrauma abstract class FishSwimParams : SwimParams, IFishAnimation { - [Serialize(false, true, description: "TODO"), Editable] + [Serialize(false, true, description: "Instead of linear movement (default), use a wave-like movement. Note: WaveAmplitude and WaveLength don't have any effect on this. It's synced with the movement speed."), Editable] public bool UseSineMovement { get; set; } [Editable, Serialize(true, true, description: "Should the character be flipped depending on which direction it faces. Should usually be enabled on all characters that have distinctive upper and lower sides.")] @@ -149,7 +149,7 @@ namespace Barotrauma [Editable, Serialize(true, true, description: "If enabled, the character will simply be mirrored horizontally when it wants to turn around. If disabled, it will rotate itself to face the other direction.")] public bool Mirror { get; set; } - [Serialize(1f, true), Editable] + [Serialize(5f, true), Editable] public float WaveAmplitude { get; set; } [Serialize(10.0f, true), Editable] @@ -167,6 +167,9 @@ namespace Barotrauma [Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)] public float TailTorque { get; set; } + [Serialize(1f, true, description: "Multiplier applied based on the angle difference between the tail and the main limb. Increasing the value prevents snake-like characters from getting tangled on themselves. Default = 1 (no boost)"), Editable(MinValueFloat = 1, MaxValueFloat = 100)] + public float TailTorqueMultiplier { get; set; } + [Serialize(25.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)] public float FootTorque { get; set; } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/Source/Characters/Params/CharacterParams.cs index 931a794ec..5b5380f19 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Params/CharacterParams.cs @@ -19,6 +19,9 @@ namespace Barotrauma [Serialize("", true), Editable] public string SpeciesName { get; private set; } + [Serialize("", true, description: "If the display name is not defined, the game first tries to find the translated name. If that is not found, the species name will be used."), Editable] + public string DisplayName { get; private set; } + [Serialize("", true, description: "If defined, different species of the same group are considered like the characters of the same species by the AI."), Editable] public string Group { get; private set; } @@ -46,6 +49,7 @@ namespace Barotrauma public readonly List Sounds = new List(); public readonly List BloodEmitters = new List(); public readonly List GibEmitters = new List(); + public readonly List DamageEmitters = new List(); public readonly List Inventories = new List(); public HealthParams Health { get; private set; } public AIParams AI { get; private set; } @@ -124,6 +128,12 @@ namespace Barotrauma GibEmitters.Add(emitter); SubParams.Add(emitter); } + foreach (var element in MainElement.GetChildElements("damageemitter")) + { + var emitter = new ParticleParams(element, this); + GibEmitters.Add(emitter); + SubParams.Add(emitter); + } foreach (var soundElement in MainElement.GetChildElements("sound")) { var sound = new SoundParams(soundElement, this); @@ -193,6 +203,7 @@ namespace Barotrauma public void AddBloodEmitter() => AddEmitter("bloodemitter"); public void AddGibEmitter() => AddEmitter("gibemitter"); + public void AddDamageEmitter() => AddEmitter("damageemitter"); private void AddEmitter(string type) { @@ -204,6 +215,9 @@ namespace Barotrauma case "bloodemitter": TryAddSubParam(new XElement(type), (e, c) => new ParticleParams(e, c), out _, BloodEmitters); break; + case "damageemitter": + TryAddSubParam(new XElement(type), (e, c) => new ParticleParams(e, c), out _, DamageEmitters); + break; default: throw new NotImplementedException(type); } } @@ -211,6 +225,7 @@ namespace Barotrauma public bool RemoveSound(SoundParams soundParams) => RemoveSubParam(soundParams); public bool RemoveBloodEmitter(ParticleParams emitter) => RemoveSubParam(emitter, BloodEmitters); public bool RemoveGibEmitter(ParticleParams emitter) => RemoveSubParam(emitter, GibEmitters); + public bool RemoveDamageEmitter(ParticleParams emitter) => RemoveSubParam(emitter, DamageEmitters); public bool RemoveInventory(InventoryParams inventory) => RemoveSubParam(inventory, Inventories); protected bool RemoveSubParam(T subParam, IList collection = null) where T : SubParam @@ -333,16 +348,16 @@ namespace Barotrauma [Serialize(false, true)] public bool UseHealthWindow { get; set; } - [Serialize(0f, true, description: "How easily the character heals from the bleeding wounds. Default 0 (no extra healing)."), Editable(MinValueFloat = 0, MaxValueFloat = 10)] + [Serialize(0f, true, description: "How easily the character heals from the bleeding wounds. Default 0 (no extra healing)."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float BleedingReduction { get; private set; } - [Serialize(0f, true, description: "How easily the character heals from the burn wounds. Default 0 (no extra healing)."), Editable(MinValueFloat = 0, MaxValueFloat = 10)] + [Serialize(0f, true, description: "How easily the character heals from the burn wounds. Default 0 (no extra healing)."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float BurnReduction { get; private set; } - [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10)] + [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float ConstantHealthRegeneration { get; private set; } - [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10)] + [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)] public float HealthRegenerationWhenEating { get; private set; } // TODO: limbhealths, sprite? @@ -411,10 +426,10 @@ namespace Barotrauma [Serialize(1.0f, true, description: "Affects how far the character can hear the targets. Used as a multiplier."), Editable(minValue: 0f, maxValue: 10f)] public float Hearing { get; private set; } - [Serialize(100f, true, description: "How much the target priority increase when the character takes damage? Additive."), Editable(minValue: -1000f, maxValue: 1000f)] + [Serialize(100f, true, description: "How much the targeting priority increases each time the character takes damage. Works like the greed value, described above. The default value is 100."), Editable(minValue: -1000f, maxValue: 1000f)] public float AggressionHurt { get; private set; } - [Serialize(10f, true, description: "How much the target priority increase when the character takes damage? Additive."), Editable(minValue: 0f, maxValue: 1000f)] + [Serialize(10f, true, description: "How much the targeting priority increases each time the character does damage to the target. The actual priority adjustment is calculated based on the damage percentage multiplied by the greed value. The default value is 10, which means the priority will increase by 1 every time the character does damage 10% of the target's current health. If the damage is 50%, then the priority increase is 5."), Editable(minValue: 0f, maxValue: 1000f)] public float AggressionGreed { get; private set; } [Serialize(0f, true, description: "If the health drops below this threshold, the character flees. In percentages."), Editable(minValue: 0f, maxValue: 100f)] @@ -423,8 +438,8 @@ namespace Barotrauma [Serialize(false, true, description: "Does the character attack ONLY when provoked?"), Editable()] public bool AttackOnlyWhenProvoked { get; private set; } - [Serialize(true, true, description: "When true, the character retaliates quickly when it's taking damage. Enabled by default."), Editable] - public bool RetaliateWhenTakingDamage { get; private set; } + [Serialize(true, true, description: "The character will flee for a brief moment when being shot at if not performing an attack."), Editable] + public bool AvoidGunfire { get; private set; } [Serialize(false, true, description: "Does the character try to break inside the sub?"), Editable()] public bool AggressiveBoarding { get; private set; } diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/Source/Characters/Params/Ragdoll/RagdollParams.cs index 47fcd7ca1..f400d2a70 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Params/Ragdoll/RagdollParams.cs @@ -34,9 +34,19 @@ namespace Barotrauma [Serialize("", true, description: "Default path for the limb sprite textures. Used only if the limb specific path for the limb is not defined"), Editable] public string Texture { get; set; } - [Serialize(0f, true, description: "The orientation of the sprites as drawn on the sprite sheet. Can be overridden by setting a value for Limb's 'Sprite Orientation'. Used mainly for animations and widgets."), Editable(-360, 360)] + [Serialize(0.0f, true, description: "The orientation of the sprites as drawn on the sprite sheet. Can be overridden by setting a value for Limb's 'Sprite Orientation'. Used mainly for animations and widgets."), Editable(-360, 360)] public float SpritesheetOrientation { get; set; } + public bool IsSpritesheetOrientationHorizontal + { + get + { + return + (SpritesheetOrientation > 45.0f && SpritesheetOrientation < 135.0f) || + (SpritesheetOrientation > 255.0f && SpritesheetOrientation < 315.0f); + } + } + private float limbScale; [Serialize(1.0f, true), Editable(MIN_SCALE, MAX_SCALE, DecimalCount = 3)] public float LimbScale { get { return limbScale; } set { limbScale = MathHelper.Clamp(value, MIN_SCALE, MAX_SCALE); } } @@ -55,12 +65,18 @@ namespace Barotrauma [Serialize(50f, true, description: "How much impact is required before the character takes impact damage?"), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] public float ImpactTolerance { get; set; } - [Serialize(true, true, description: "Can the creature enter submarine and walk when there is no water? Creatures that cannot enter submarines, always collide with it, even when there is a gap."), Editable()] + [Serialize(true, true, description: "Can the creature enter submarine. Creatures that cannot enter submarines, always collide with it, even when there is a gap."), Editable()] public bool CanEnterSubmarine { get; set; } + [Serialize(true, true), Editable] + public bool CanWalk { get; set; } + [Serialize(true, true, description: "Can the character be dragged around by other creatures?"), Editable()] public bool Draggable { get; set; } + [Serialize(LimbType.Torso, true), Editable] + public LimbType MainLimb { get; set; } + private static Dictionary> allRagdolls = new Dictionary>(); public List Colliders { get; private set; } = new List(); @@ -513,6 +529,9 @@ namespace Barotrauma [Serialize(float.NaN, true, description: "The orientation of the sprite as drawn on the sprite sheet. Overrides the value defined in the Ragdoll settings. Used mainly for animations and widgets."), Editable(-360, 360)] public float SpriteOrientation { get; set; } + /// + /// The orientation of the sprite as drawn on the sprite sheet (in radians). + /// public float GetSpriteOrientation() => MathHelper.ToRadians(float.IsNaN(SpriteOrientation) ? Ragdoll.SpritesheetOrientation : SpriteOrientation); [Serialize(true, true, description: "Does the limb flip when the character flips?"), Editable()] @@ -527,7 +546,7 @@ namespace Barotrauma [Serialize(false, true, description: "Disable drawing for this limb."), Editable()] public bool Hide { get; set; } - [Serialize(1f, true, description: "Higher values make AI characters prefer attacking this limb."), Editable()] + [Serialize(1f, true, description: "Higher values make AI characters prefer attacking this limb."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 10)] public float AttackPriority { get; set; } [Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500)] diff --git a/Barotrauma/BarotraumaShared/Source/ContentPackage.cs b/Barotrauma/BarotraumaShared/Source/ContentPackage.cs index e6347551c..d6df0873f 100644 --- a/Barotrauma/BarotraumaShared/Source/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/Source/ContentPackage.cs @@ -216,6 +216,8 @@ namespace Barotrauma } } + public bool NeedsRestart; + public override string ToString() { return Name; diff --git a/Barotrauma/BarotraumaShared/Source/DebugConsole.cs b/Barotrauma/BarotraumaShared/Source/DebugConsole.cs index 9f4b54c52..854c87bd0 100644 --- a/Barotrauma/BarotraumaShared/Source/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/Source/DebugConsole.cs @@ -79,7 +79,7 @@ namespace Barotrauma } } - private static Queue queuedMessages = new Queue(); + private static readonly Queue queuedMessages = new Queue(); static partial void ShowHelpMessage(Command command); @@ -90,7 +90,7 @@ namespace Barotrauma public delegate void QuestionCallback(string answer); private static QuestionCallback activeQuestionCallback; - private static List commands = new List(); + private static readonly List commands = new List(); public static List Commands { get { return commands; } @@ -100,12 +100,12 @@ namespace Barotrauma private static int currentAutoCompletedIndex; //used for keeping track of the message entered when pressing up/down - static int selectedIndex; + private static int selectedIndex; public static bool CheatsEnabled; - private static List unsavedMessages = new List(); - private static int messagesPerFile = 5000; + private static readonly List unsavedMessages = new List(); + private static readonly int messagesPerFile = 5000; public const string SavePath = "ConsoleLogs"; private static void AssignOnExecute(string names, Action onExecute) @@ -113,7 +113,7 @@ namespace Barotrauma var matchingCommand = commands.Find(c => c.names.Intersect(names.Split('|')).Count() > 0); if (matchingCommand == null) { - throw new Exception("AssignOnExecute failed. Command matching the name(s) \""+names+"\" not found."); + throw new Exception("AssignOnExecute failed. Command matching the name(s) \"" + names + "\" not found."); } else { @@ -165,8 +165,7 @@ namespace Barotrauma NewMessage("***************", Color.Cyan); foreach (MapEntityPrefab ep in MapEntityPrefab.List) { - var itemPrefab = ep as ItemPrefab; - if (itemPrefab == null || itemPrefab.Name == null) continue; + if (!(ep is ItemPrefab itemPrefab) || itemPrefab.Name == null) continue; string text = $"- {itemPrefab.Name}"; if (itemPrefab.Tags.Any()) { @@ -284,6 +283,8 @@ namespace Barotrauma NewMessage("Enemy AI enabled", Color.Green); }, isCheat: true)); + commands.Add(new Command("starttraitormissionimmediately", "starttraitormissionimmediately: Skip the initial delay of the traitor mission and start one immediately.", null)); + commands.Add(new Command("botcount", "botcount [x]: Set the number of bots in the crew in multiplayer.", null)); commands.Add(new Command("botspawnmode", "botspawnmode [fill/normal]: Set how bots are spawned in the multiplayer.", null)); @@ -335,18 +336,19 @@ namespace Barotrauma commands.Add(new Command("kick", "kick [name]: Kick a player out of the server.", (string[] args) => { - if (GameMain.NetworkMember == null || args.Length == 0) return; + if (GameMain.NetworkMember == null || args.Length == 0) { return; } string playerName = string.Join(" ", args); - ShowQuestionPrompt("Reason for kicking \"" + playerName + "\"?", (reason) => + ShowQuestionPrompt("Reason for kicking \"" + playerName + "\"? (Enter c to cancel)", (reason) => { + if (reason == "c" || reason == "C") { return; } GameMain.NetworkMember.KickPlayer(playerName, reason); }); }, () => { - if (GameMain.NetworkMember == null) return null; + if (GameMain.NetworkMember == null) { return null; } return new string[][] { @@ -366,8 +368,9 @@ namespace Barotrauma return; } - ShowQuestionPrompt("Reason for kicking \"" + client.Name + "\"?", (reason) => + ShowQuestionPrompt("Reason for kicking \"" + client.Name + "\"? (Enter c to cancel)", (reason) => { + if (reason == "c" || reason == "C") { return; } GameMain.NetworkMember.KickPlayer(client.Name, reason); }); })); @@ -377,10 +380,12 @@ namespace Barotrauma if (GameMain.NetworkMember == null || args.Length == 0) return; string clientName = string.Join(" ", args); - ShowQuestionPrompt("Reason for banning \"" + clientName + "\"?", (reason) => + ShowQuestionPrompt("Reason for banning \"" + clientName + "\"? (Enter c to cancel)", (reason) => { - ShowQuestionPrompt("Enter the duration of the ban (leave empty to ban permanently, or use the format \"[days] d [hours] h\")", (duration) => + if (reason == "c" || reason == "C") { return; } + ShowQuestionPrompt("Enter the duration of the ban (leave empty to ban permanently, or use the format \"[days] d [hours] h\") (Enter c to cancel)", (duration) => { + if (duration == "c" || duration == "C") { return; } TimeSpan? banDuration = null; if (!string.IsNullOrWhiteSpace(duration)) { @@ -418,10 +423,12 @@ namespace Barotrauma return; } - ShowQuestionPrompt("Reason for banning \"" + client.Name + "\"?", (reason) => + ShowQuestionPrompt("Reason for banning \"" + client.Name + "\"? (Enter c to cancel)", (reason) => { - ShowQuestionPrompt("Enter the duration of the ban (leave empty to ban permanently, or use the format \"[days] d [hours] h\")", (duration) => + if (reason == "c" || reason == "C") { return; } + ShowQuestionPrompt("Enter the duration of the ban (leave empty to ban permanently, or use the format \"[days] d [hours] h\") (c to cancel)", (duration) => { + if (duration == "c" || duration == "C") { return; } TimeSpan? banDuration = null; if (!string.IsNullOrWhiteSpace(duration)) { @@ -874,8 +881,7 @@ namespace Barotrauma commands.Add(new Command("campaigninfo|campaignstatus", "campaigninfo: Display information about the state of the currently active campaign.", (string[] args) => { - var campaign = GameMain.GameSession?.GameMode as CampaignMode; - if (campaign == null) + if (!(GameMain.GameSession?.GameMode is CampaignMode campaign)) { ThrowError("No campaign active!"); return; @@ -886,8 +892,7 @@ namespace Barotrauma commands.Add(new Command("campaigndestination|setcampaigndestination", "campaigndestination [index]: Set the location to head towards in the currently active campaign.", (string[] args) => { - var campaign = GameMain.GameSession?.GameMode as CampaignMode; - if (campaign == null) + if (!(GameMain.GameSession?.GameMode is CampaignMode campaign)) { ThrowError("No campaign active!"); return; @@ -938,7 +943,6 @@ namespace Barotrauma NewMessage((GameSettings.VerboseLogging ? "Enabled" : "Disabled") + " verbose logging.", Color.White); }, isCheat: false)); - commands.Add(new Command("calculatehashes", "calculatehashes [content package name]: Show the MD5 hashes of the files in the selected content package. If the name parameter is omitted, the first content package is selected.", (string[] args) => { if (args.Length > 0) @@ -967,7 +971,21 @@ namespace Barotrauma }; })); -#if DEBUG + commands.Add(new Command("debugai", "", onExecute: (string[] args) => + { + var commands = new List>() + { + new KeyValuePair("debugdraw", new string[]{ "true" }), + new KeyValuePair("los", new string[]{ "false" }), + new KeyValuePair("lights", new string[]{ "false" }), + new KeyValuePair("freecam", new string[0]), + }; + foreach (var command in commands) + { + Commands.Find(c => c.names.Any(n => n.Equals(command.Key, StringComparison.OrdinalIgnoreCase)))?.Execute(command.Value); + } + })); + commands.Add(new Command("simulatedlatency", "simulatedlatency [minimumlatencyseconds] [randomlatencyseconds]: applies a simulated latency to network messages. Useful for simulating real network conditions when testing the multiplayer locally.", (string[] args) => { if (args.Count() < 2 || (GameMain.NetworkMember == null)) return; @@ -1039,7 +1057,6 @@ namespace Barotrauma #endif NewMessage("Set packet duplication to " + (int)(duplicates * 100) + "%.", Color.White); })); -#endif //"dummy commands" that only exist so that the server can give clients permissions to use them //TODO: alphabetical order? @@ -1170,16 +1187,6 @@ namespace Barotrauma } } - private static string AutoCompleteStr(string str, IEnumerable validStrings) - { - if (string.IsNullOrEmpty(str)) return str; - foreach (string validStr in validStrings) - { - if (validStr.Length > str.Length && validStr.Substring(0, str.Length) == str) return validStr; - } - return str; - } - public static void ResetAutoComplete() { currentAutoCompletedCommand = ""; @@ -1197,7 +1204,7 @@ namespace Barotrauma { selectedIndex += direction; if (selectedIndex < 0) selectedIndex = Messages.Count - 1; - selectedIndex = selectedIndex % Messages.Count; + selectedIndex %= Messages.Count; if (++i >= Messages.Count) break; } while (!Messages[selectedIndex].IsCommand || Messages[selectedIndex].Text == currentText); @@ -1257,13 +1264,6 @@ namespace Barotrauma return; } -#if !DEBUG - if (!IsCommandPermitted(splitCommand[0].ToLowerInvariant(), GameMain.Client)) - { - ThrowError("You're not permitted to use the command \"" + splitCommand[0].ToLowerInvariant() + "\"!"); - return; - } -#endif } #endif @@ -1509,9 +1509,7 @@ namespace Barotrauma public static void NewMessage(string msg, Color color, bool isCommand = false) { - if (string.IsNullOrEmpty((msg))) return; - - var newMsg = new ColoredText(msg, color, isCommand); + if (string.IsNullOrEmpty(msg)) { return; } lock (queuedMessages) { diff --git a/Barotrauma/BarotraumaShared/Source/Events/EventManagerSettings.cs b/Barotrauma/BarotraumaShared/Source/Events/EventManagerSettings.cs index 317d69926..0b669736a 100644 --- a/Barotrauma/BarotraumaShared/Source/Events/EventManagerSettings.cs +++ b/Barotrauma/BarotraumaShared/Source/Events/EventManagerSettings.cs @@ -10,6 +10,7 @@ namespace Barotrauma { public static readonly List List = new List(); + public readonly string Identifier; public readonly string Name; //How much the event threshold increases per second. 0.0005f = 0.03f per minute @@ -48,28 +49,30 @@ namespace Barotrauma foreach (XElement subElement in mainElement.Elements()) { var element = subElement.IsOverride() ? subElement.FirstElement() : subElement; - string name = element.Name.ToString(); - var duplicate = List.FirstOrDefault(e => e.Name.ToString().Equals(name, StringComparison.OrdinalIgnoreCase)); + string identifier = element.Name.ToString(); + var duplicate = List.FirstOrDefault(e => e.Identifier.ToString().Equals(identifier, StringComparison.OrdinalIgnoreCase)); if (duplicate != null) { if (allowOverriding || subElement.IsOverride()) { - DebugConsole.NewMessage($"Overriding the existing preset '{name}' in the event manager settings using the file '{file}'", Color.Yellow); + DebugConsole.NewMessage($"Overriding the existing preset '{identifier}' in the event manager settings using the file '{file}'", Color.Yellow); List.Remove(duplicate); } else { - DebugConsole.ThrowError($"Error in '{file}': Another element with the name '{name}' found! Each element must have a unique name. Use tags if you want to override an existing preset."); + DebugConsole.ThrowError($"Error in '{file}': Another element with the name '{identifier}' found! Each element must have a unique name. Use tags if you want to override an existing preset."); continue; } } List.Add(new EventManagerSettings(element)); } + List.Sort((x, y) => { return Math.Sign((x.MinLevelDifficulty + x.MaxLevelDifficulty) / 2.0f - (y.MinLevelDifficulty + y.MaxLevelDifficulty) / 2.0f); }); } public EventManagerSettings(XElement element) { - Name = element.Name.ToString(); + Identifier = element.Name.ToString(); + Name = TextManager.Get("difficulty." + Identifier, returnNull: true) ?? Identifier; EventThresholdIncrease = element.GetAttributeFloat("EventThresholdIncrease", 0.0005f); DefaultEventThreshold = element.GetAttributeFloat("DefaultEventThreshold", 0.2f); EventCooldown = element.GetAttributeFloat("EventCooldown", 360.0f); diff --git a/Barotrauma/BarotraumaShared/Source/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/Source/Events/Missions/Mission.cs index a518a9bc0..b1356b31a 100644 --- a/Barotrauma/BarotraumaShared/Source/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/Source/Events/Missions/Mission.cs @@ -9,7 +9,25 @@ namespace Barotrauma { public readonly MissionPrefab Prefab; protected bool completed; - + protected int state; + public int State + { + get { return state; } + protected set + { + if (state != value) + { + state = value; +#if SERVER + GameMain.Server?.UpdateMissionState(state); +#endif + ShowMessage(State); + } + } + } + + protected bool IsClient => GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; + public readonly List Headers; public readonly List Messages; @@ -107,17 +125,13 @@ namespace Barotrauma public static Mission LoadRandom(Location[] locations, MTRandom rand, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false) { List allowedMissions = new List(); - if (missionType == MissionType.Random) - { - allowedMissions.AddRange(MissionPrefab.List); - } - else if (missionType == MissionType.None) + if (missionType == MissionType.None) { return null; } else { - allowedMissions = MissionPrefab.List.FindAll(m => m.type == missionType); + allowedMissions.AddRange(MissionPrefab.List.Where(m => ((int)(missionType & m.type)) != 0)); } allowedMissions.RemoveAll(m => isSinglePlayer ? m.MultiplayerOnly : m.SingleplayerOnly); diff --git a/Barotrauma/BarotraumaShared/Source/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/Source/Events/Missions/MissionPrefab.cs index 31bcae2e1..231fe64fc 100644 --- a/Barotrauma/BarotraumaShared/Source/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/Source/Events/Missions/MissionPrefab.cs @@ -6,14 +6,15 @@ using Microsoft.Xna.Framework; namespace Barotrauma { + [Flags] public enum MissionType { - Random, - None, - Salvage, - Monster, - Cargo, - Combat + None = 0x0, + Salvage = 0x1, + Monster = 0x2, + Cargo = 0x4, + Combat = 0x8, + All = 0xf } partial class MissionPrefab @@ -155,11 +156,6 @@ namespace Barotrauma DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - \"" + missionTypeName + "\" is not a valid mission type."); return; } - if (type == MissionType.Random) - { - DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - mission type cannot be random."); - return; - } if (type == MissionType.None) { DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - mission type cannot be none."); diff --git a/Barotrauma/BarotraumaShared/Source/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaShared/Source/Events/Missions/MonsterMission.cs index 66a92c012..750b7fd9a 100644 --- a/Barotrauma/BarotraumaShared/Source/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaShared/Source/Events/Missions/MonsterMission.cs @@ -1,34 +1,41 @@ using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Linq; +using System; namespace Barotrauma { class MonsterMission : Mission { - private string monsterFile; - - private int state; - - private int monsterCount; - + private readonly string monsterFile; + private readonly int monsterCount; + private readonly HashSet> monsterFiles = new HashSet>(); private readonly List monsters = new List(); private readonly List sonarPositions = new List(); - public override IEnumerable SonarPositions - { - get - { - return sonarPositions; - } - } + public override IEnumerable SonarPositions => sonarPositions; public MonsterMission(MissionPrefab prefab, Location[] locations) : base(prefab, locations) { - monsterFile = prefab.ConfigElement.GetAttributeString("monsterfile", ""); + monsterFile = prefab.ConfigElement.GetAttributeString("monsterfile", null); monsterCount = prefab.ConfigElement.GetAttributeInt("monstercount", 1); - + foreach (var monsterElement in prefab.ConfigElement.GetChildElements("monster")) + { + string monster = monsterElement.GetAttributeString("character", string.Empty); + if (monsterFile == null) + { + monsterFile = monster; + } + int defaultCount = monsterElement.GetAttributeInt("count", -1); + if (defaultCount < 0) + { + defaultCount = monsterElement.GetAttributeInt("amount", 1); + } + int min = monsterElement.GetAttributeInt("min", defaultCount); + int max = Math.Max(min, monsterElement.GetAttributeInt("max", defaultCount)); + monsterFiles.Add(new Tuple(monster, Rand.Range(min, max + 1, Rand.RandSync.Server))); + } description = description.Replace("[monster]", TextManager.Get("character." + System.IO.Path.GetFileNameWithoutExtension(monsterFile))); } @@ -37,11 +44,22 @@ namespace Barotrauma { Level.Loaded.TryGetInterestingPosition(true, Level.PositionType.MainPath, Level.Loaded.Size.X * 0.3f, out Vector2 spawnPos); - bool isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; - for (int i = 0; i < monsterCount; i++) + bool isClient = IsClient; + if (!string.IsNullOrEmpty(monsterFile)) { - monsters.Add(Character.Create(monsterFile, spawnPos, ToolBox.RandomSeed(8), null, isClient, true, false)); + for (int i = 0; i < monsterCount; i++) + { + monsters.Add(Character.Create(monsterFile, spawnPos, ToolBox.RandomSeed(8), null, isClient, true, false)); + } } + foreach (var monster in monsterFiles) + { + for (int i = 0; i < monster.Item2; i++) + { + monsters.Add(Character.Create(monster.Item1, spawnPos, ToolBox.RandomSeed(8), null, isClient, true, false)); + } + } + monsters.ForEach(m => m.Enabled = false); SwarmBehavior.CreateSwarm(monsters.Cast()); sonarPositions.Add(spawnPos); @@ -49,40 +67,35 @@ namespace Barotrauma public override void Update(float deltaTime) { - switch (state) + switch (State) { case 0: sonarPositions.Clear(); - var activeMonsters = monsters.Where(m => m != null && !m.Removed && !m.IsDead); - if (activeMonsters.Any()) + foreach (var monster in monsters) { - Vector2 centerOfMass = Vector2.Zero; - foreach (var monster in activeMonsters) + if (monster.Removed || monster.IsDead) { continue; } + //don't add another label if there's another monster roughly at the same spot + if (sonarPositions.All(p => Vector2.DistanceSquared(p, monster.Position) > 1000.0f * 1000.0f)) { - //don't add another label if there's another monster roughly at the same spot - if (sonarPositions.All(p => Vector2.DistanceSquared(p, monster.Position) > 1000.0f * 1000.0f)) - { - sonarPositions.Add(monster.Position); - } + sonarPositions.Add(monster.Position); } } - - - if (activeMonsters.Any()) { return; } - - ShowMessage(state); - - state = 1; + if (!IsClient && monsters.All(m => IsEliminated(m))) + { + State = 1; + } break; } } public override void End() { - if (!monsters.All(m => m.Removed || m.IsDead)) { return; } + if (State < 1) { return; } GiveReward(); completed = true; } + + public bool IsEliminated(Character enemy) => enemy.Removed || enemy.IsDead || enemy.AIController is EnemyAIController ai && ai.State == AIState.Flee; } } diff --git a/Barotrauma/BarotraumaShared/Source/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/Source/Events/Missions/SalvageMission.cs index 0f8e2459d..2ff3d82a5 100644 --- a/Barotrauma/BarotraumaShared/Source/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/Source/Events/Missions/SalvageMission.cs @@ -14,13 +14,11 @@ namespace Barotrauma private Level.PositionType spawnPositionType; - private int state; - public override IEnumerable SonarPositions { get { - if (state > 0 ) + if (State > 0 ) { Enumerable.Empty(); } @@ -87,34 +85,27 @@ namespace Barotrauma public override void Update(float deltaTime) { - switch (state) + if (IsClient) { return; } + switch (State) { case 0: - //item.body.LinearVelocity = Vector2.Zero; - if (item.ParentInventory != null) item.body.FarseerBody.IsKinematic = false; - if (item.CurrentHull?.Submarine == null) return; - - ShowMessage(state); - - state = 1; + if (item.ParentInventory != null) { item.body.FarseerBody.IsKinematic = false; } + if (item.CurrentHull?.Submarine == null) { return; } + State = 1; break; case 1: - if (!Submarine.MainSub.AtEndPosition && !Submarine.MainSub.AtStartPosition) return; - - ShowMessage(state); - - state = 2; + if (!Submarine.MainSub.AtEndPosition && !Submarine.MainSub.AtStartPosition) { return; } + State = 2; break; } } public override void End() { - if (item.CurrentHull?.Submarine == null || !item.CurrentHull.Submarine.AtEndPosition || item.Removed) return; + if (item.CurrentHull?.Submarine == null || !item.CurrentHull.Submarine.AtEndPosition || item.Removed) { return; } + item.Remove(); - GiveReward(); - completed = true; } } diff --git a/Barotrauma/BarotraumaShared/Source/Extensions/StringFormatter.cs b/Barotrauma/BarotraumaShared/Source/Extensions/StringFormatter.cs index f13e5a0de..dff706002 100644 --- a/Barotrauma/BarotraumaShared/Source/Extensions/StringFormatter.cs +++ b/Barotrauma/BarotraumaShared/Source/Extensions/StringFormatter.cs @@ -110,5 +110,60 @@ namespace Barotrauma } } } + + public static ICollection ParseCommaSeparatedStringToCollection(string input, ICollection texts = null, bool convertToLowerInvariant = true) + { + if (texts == null) + { + texts = new HashSet(); + } + else + { + texts.Clear(); + } + if (!string.IsNullOrWhiteSpace(input)) + { + foreach (string value in input.Split(',')) + { + if (string.IsNullOrWhiteSpace(value)) { continue; } + if (convertToLowerInvariant) + { + texts.Add(value.ToLowerInvariant()); + } + else + { + texts.Add(value); + } + } + } + return texts; + } + + public static ICollection ParseSeparatedStringToCollection(string input, string[] separators, ICollection texts = null, bool convertToLowerInvariant = true) + { + if (texts == null) + { + texts = new HashSet(); + } + else + { + texts.Clear(); + } + if (!string.IsNullOrWhiteSpace(input)) + { + foreach (string value in input.Split(separators, StringSplitOptions.RemoveEmptyEntries)) + { + if (convertToLowerInvariant) + { + texts.Add(value.ToLowerInvariant()); + } + else + { + texts.Add(value); + } + } + } + return texts; + } } } diff --git a/Barotrauma/BarotraumaShared/Source/GameAnalyticsManager.cs b/Barotrauma/BarotraumaShared/Source/GameAnalyticsManager.cs index cb6b1df83..b634bf35a 100644 --- a/Barotrauma/BarotraumaShared/Source/GameAnalyticsManager.cs +++ b/Barotrauma/BarotraumaShared/Source/GameAnalyticsManager.cs @@ -83,8 +83,16 @@ namespace Barotrauma /// public static void AddErrorEventOnce(string identifier, EGAErrorSeverity errorSeverity, string message) { - if (!GameSettings.SendUserStatistics) return; - if (sentEventIdentifiers.Contains(identifier)) return; + if (!GameSettings.SendUserStatistics) { return; } + if (sentEventIdentifiers.Contains(identifier)) { return; } + + if (GameMain.SelectedPackages != null) + { + if (GameMain.VanillaContent == null || GameMain.SelectedPackages.Any(p => p.HasMultiplayerIncompatibleContent && p != GameMain.VanillaContent)) + { + message = "[MODDED] " + message; + } + } GameAnalytics.AddErrorEvent(errorSeverity, message); sentEventIdentifiers.Add(identifier); diff --git a/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/MultiPlayerCampaign.cs index 9c7f8c86d..aaa343799 100644 --- a/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/Source/GameSession/GameModes/MultiPlayerCampaign.cs @@ -104,6 +104,7 @@ namespace Barotrauma { if (c.Character?.Info != null && !c.Character.IsDead) { + c.CharacterInfo = c.Character.Info; characterData.Add(new CharacterCampaignData(c)); } } diff --git a/Barotrauma/BarotraumaShared/Source/GameSettings.cs b/Barotrauma/BarotraumaShared/Source/GameSettings.cs index 8a1cc5411..e781bcc15 100644 --- a/Barotrauma/BarotraumaShared/Source/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/Source/GameSettings.cs @@ -51,6 +51,8 @@ namespace Barotrauma public bool VoipAttenuationEnabled { get; set; } public bool UseDirectionalVoiceChat { get; set; } + public IList CaptureDeviceNames; + public enum VoiceMode { Disabled, @@ -69,7 +71,7 @@ namespace Barotrauma private LosMode losMode; - public List jobPreferences; + public List> jobPreferences; private bool useSteamMatchmaking; private bool requireSteamAuthentication; @@ -119,7 +121,7 @@ namespace Barotrauma } } - public List JobPreferences + public List> JobPreferences { get { return jobPreferences; } set { jobPreferences = value; } @@ -211,12 +213,13 @@ namespace Barotrauma } } + public const float MaxMicrophoneVolume = 10.0f; public float MicrophoneVolume { get { return microphoneVolume; } set { - microphoneVolume = MathHelper.Clamp(value, 0.2f, 10.0f); + microphoneVolume = MathHelper.Clamp(value, 0.2f, MaxMicrophoneVolume); } } public string Language @@ -232,6 +235,7 @@ namespace Barotrauma if (!SelectedContentPackages.Contains(contentPackage)) { SelectedContentPackages.Add(contentPackage); + contentPackage.NeedsRestart |= contentPackage.HasMultiplayerIncompatibleContent; ContentPackage.SortContentPackages(); } } @@ -241,6 +245,7 @@ namespace Barotrauma if (SelectedContentPackages.Contains(contentPackage)) { SelectedContentPackages.Remove(contentPackage); + contentPackage.NeedsRestart |= contentPackage.HasMultiplayerIncompatibleContent; ContentPackage.SortContentPackages(); } } @@ -442,11 +447,7 @@ namespace Barotrauma GraphicsHeight = 768; MasterServerUrl = ""; SelectContentPackage(ContentPackage.List.Any() ? ContentPackage.List[0] : new ContentPackage("")); - jobPreferences = new List(); - foreach (string job in JobPrefab.List.Keys) - { - jobPreferences.Add(job); - } + jobPreferences = new List>(); return; } @@ -570,9 +571,12 @@ namespace Barotrauma var gameplay = new XElement("gameplay"); var jobPreferences = new XElement("jobpreferences"); - foreach (string jobName in JobPreferences) + foreach (Pair job in JobPreferences) { - jobPreferences.Add(new XElement("job", new XAttribute("identifier", jobName))); + XElement jobElement = new XElement("job"); + jobElement.Add(new XAttribute("identifier", job.First)); + jobElement.Add(new XAttribute("variant", job.Second)); + jobPreferences.Add(jobElement); } gameplay.Add(jobPreferences); doc.Root.Add(gameplay); @@ -890,9 +894,12 @@ namespace Barotrauma var gameplay = new XElement("gameplay"); var jobPreferences = new XElement("jobpreferences"); - foreach (string jobName in JobPreferences) + foreach (Pair job in JobPreferences) { - jobPreferences.Add(new XElement("job", new XAttribute("identifier", jobName))); + XElement jobElement = new XElement("job"); + jobElement.Add(new XAttribute("identifier", job.First)); + jobElement.Add(new XAttribute("variant", job.Second)); + jobPreferences.Add(jobElement); } gameplay.Add(jobPreferences); doc.Root.Add(gameplay); @@ -972,14 +979,19 @@ namespace Barotrauma CampaignDisclaimerShown = doc.Root.GetAttributeBool("campaigndisclaimershown", CampaignDisclaimerShown); EditorDisclaimerShown = doc.Root.GetAttributeBool("editordisclaimershown", EditorDisclaimerShown); XElement gameplayElement = doc.Root.Element("gameplay"); + jobPreferences = new List>(); if (gameplayElement != null) { - jobPreferences = new List(); - foreach (XElement ele in gameplayElement.Element("jobpreferences").Elements("job")) + var preferencesElement = gameplayElement.Element("jobpreferences"); + if (preferencesElement != null) { - string jobIdentifier = ele.GetAttributeString("identifier", ""); - if (string.IsNullOrEmpty(jobIdentifier)) continue; - jobPreferences.Add(jobIdentifier); + foreach (XElement ele in preferencesElement.Elements("job")) + { + string jobIdentifier = ele.GetAttributeString("identifier", ""); + int outfitVariant = ele.GetAttributeInt("variant", 1); + if (string.IsNullOrEmpty(jobIdentifier)) continue; + jobPreferences.Add(new Pair(jobIdentifier, outfitVariant)); + } } } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Door.cs index 136b9513f..d69d69a2b 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Door.cs @@ -20,9 +20,9 @@ namespace Barotrauma.Items.Components private bool isOpen; private float openState; - private Sprite doorSprite, weldedSprite, brokenSprite; - private bool scaleBrokenSprite, fadeBrokenSprite; - private bool autoOrientGap; + private readonly Sprite doorSprite, weldedSprite, brokenSprite; + private readonly bool scaleBrokenSprite, fadeBrokenSprite; + private readonly bool autoOrientGap; private bool isStuck; public bool IsStuck => isStuck; @@ -221,8 +221,8 @@ namespace Barotrauma.Items.Components #endif } - private string accessDeniedTxt = TextManager.Get("AccessDenied"); - private string cannotOpenText = TextManager.Get("DoorMsgCannotOpen"); + private readonly string accessDeniedTxt = TextManager.Get("AccessDenied"); + private readonly string cannotOpenText = TextManager.Get("DoorMsgCannotOpen"); private bool hasValidIdCard; public override bool HasRequiredItems(Character character, bool addMessage, string msg = null) { @@ -272,14 +272,15 @@ namespace Barotrauma.Items.Components ToggleState(ActionType.OnUse); PickingTime = originalPickingTime; } - else if (hasRequiredItems) - { #if CLIENT + else if (hasRequiredItems && character != null && character == Character.Controlled) + { GUI.AddMessage(accessDeniedTxt, Color.Red); -#endif + } +#endif } - return item.Condition <= RepairThreshold; + return false; } public override void Update(float deltaTime, Camera cam) @@ -340,6 +341,13 @@ namespace Barotrauma.Items.Components if (!Impassable) { Body.FarseerBody.IsSensor = false; + var ce = Body.FarseerBody.ContactList; + while (ce != null && ce.Contact != null) + { + ce.Contact.Enabled = false; + ce = ce.Next; + } + PushCharactersAway(); } #if CLIENT UpdateConvexHulls(); @@ -354,6 +362,12 @@ namespace Barotrauma.Items.Components if (!Impassable) { Body.FarseerBody.IsSensor = true; + var ce = Body.FarseerBody.ContactList; + while (ce != null && ce.Contact != null) + { + ce.Contact.Enabled = false; + ce = ce.Next; + } } linkedGap.Open = 1.0f; IsOpen = false; @@ -413,15 +427,14 @@ namespace Barotrauma.Items.Components //otherwise the gap will be removed twice and cause console warnings if (!Submarine.Unloading) { - if (linkedGap != null) linkedGap.Remove(); + linkedGap?.Remove(); } - - doorSprite.Remove(); - if (weldedSprite != null) weldedSprite.Remove(); + doorSprite?.Remove(); + weldedSprite?.Remove(); #if CLIENT - if (convexHull != null) convexHull.Remove(); - if (convexHull2 != null) convexHull2.Remove(); + convexHull?.Remove(); + convexHull2?.Remove(); #endif } @@ -474,7 +487,6 @@ namespace Barotrauma.Items.Components private bool PushBodyOutOfDoorway(Character c, PhysicsBody body, int dir, Vector2 doorRectSimPos, Vector2 doorRectSimSize) { - float diff = 0.0f; if (!MathUtils.IsValid(body.SimPosition)) { DebugConsole.ThrowError("Failed to push a limb out of a doorway - position of the body (character \"" + c.Name + "\") is not valid (" + body.SimPosition + ")"); @@ -484,7 +496,8 @@ namespace Barotrauma.Items.Components " Remoteplayer: " + c.IsRemotePlayer); return false; } - + + float diff; if (IsHorizontal) { if (body.SimPosition.X < doorRectSimPos.X || body.SimPosition.X > doorRectSimPos.X + doorRectSimSize.X) { return false; } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/ElectricalDischarger.cs index df42f6c2e..76aaf4f52 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/ElectricalDischarger.cs @@ -131,7 +131,7 @@ namespace Barotrauma.Items.Components { if (charging) { - if (voltage > minVoltage || powerConsumption <= 0.0f) + if (Voltage > MinVoltage) { Discharge(); } @@ -142,8 +142,6 @@ namespace Barotrauma.Items.Components { IsActive = false; } - - voltage = 0.0f; } public override void UpdateBroken(float deltaTime, Camera cam) @@ -455,6 +453,7 @@ namespace Barotrauma.Items.Components protected override void RemoveComponentSpecific() { + base.RemoveComponentSpecific(); list.Remove(this); } } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/MeleeWeapon.cs index 49b5e065f..2610644be 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/MeleeWeapon.cs @@ -242,9 +242,19 @@ namespace Barotrauma.Items.Components User = null; } + //ignore collision if there's a wall between the user and the weapon to prevent hitting through walls + if (Submarine.PickBody(User.AnimController.AimSourceSimPos, + item.SimPosition, + collisionCategory: Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking, + allowInsideFixture: true) != null) + { + return false; + } + Character targetCharacter = null; Limb targetLimb = null; Structure targetStructure = null; + Item targetItem = null; attack?.SetUser(User); @@ -292,6 +302,19 @@ namespace Barotrauma.Items.Components } hitTargets.Add(targetStructure); } + else if (f2.Body.UserData is Item) + { + targetItem = (Item)f2.Body.UserData; + if (AllowHitMultiple) + { + if (hitTargets.Contains(targetItem)) { return true; } + } + else + { + if (hitTargets.Any(t => t is Item)) { return true; } + } + hitTargets.Add(targetItem); + } else { return false; @@ -313,6 +336,10 @@ namespace Barotrauma.Items.Components { attack.DoDamage(User, targetStructure, item.WorldPosition, 1.0f); } + else if (targetItem != null && targetItem.Prefab.DamagedByMeleeWeapons) + { + attack.DoDamage(User, targetItem, item.WorldPosition, 1.0f); + } else { return false; diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/RepairTool.cs index 9b38863f3..cee0372dd 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Holdable/RepairTool.cs @@ -127,6 +127,7 @@ namespace Barotrauma.Items.Components if (activeTimer <= 0.0f) IsActive = false; } + private List ignoredBodies = new List(); public override bool Use(float deltaTime, Character character = null) { if (character == null || character.Removed) return false; @@ -185,7 +186,7 @@ namespace Barotrauma.Items.Components (float)Math.Cos(angle), (float)Math.Sin(angle)) * Range * item.body.Dir); - List ignoredBodies = new List(); + ignoredBodies.Clear(); foreach (Limb limb in character.AnimController.Limbs) { if (Rand.Range(0.0f, 0.5f) > degreeOfSuccess) continue; @@ -438,27 +439,48 @@ namespace Barotrauma.Items.Components private float sinTime; public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { - if (!(objective.OperateTarget is Gap leak)) return true; - - Vector2 fromItemToLeak = leak.WorldPosition - item.WorldPosition; - float dist = fromItemToLeak.Length(); + if (!(objective.OperateTarget is Gap leak)) { return true; } + if (leak.Submarine == null) { return true; } + Vector2 fromCharacterToLeak = leak.WorldPosition - character.WorldPosition; + float dist = fromCharacterToLeak.Length(); + float reach = Range + ConvertUnits.ToDisplayUnits(((HumanoidAnimController)character.AnimController).ArmLength); //too far away -> consider this done and hope the AI is smart enough to move closer - if (dist > Range * 3.0f) { return true; } - - // TODO: use the collider size? - if (!character.AnimController.InWater && character.AnimController is HumanoidAnimController && - Math.Abs(fromItemToLeak.X) < 100.0f && fromItemToLeak.Y < 0.0f && fromItemToLeak.Y > -150.0f) - { - ((HumanoidAnimController)character.AnimController).Crouching = true; - } - + if (dist > reach * 2) { return true; } + character.AIController.SteeringManager.Reset(); //steer closer if almost in range - if (dist > Range) + if (dist > reach) { - Vector2 standPos = new Vector2(Math.Sign(-fromItemToLeak.X), Math.Sign(-fromItemToLeak.Y)) / 2; - if (!character.AnimController.InWater) + if (character.AnimController.InWater) { + if (character.AIController.SteeringManager is IndoorsSteeringManager indoorSteering) + { + // Swimming inside the sub + if (indoorSteering.CurrentPath != null && !indoorSteering.IsPathDirty && indoorSteering.CurrentPath.Unreachable) + { + Vector2 dir = Vector2.Normalize(fromCharacterToLeak); + character.AIController.SteeringManager.SteeringManual(deltaTime, dir); + } + else + { + character.AIController.SteeringManager.SteeringSeek(character.GetRelativeSimPosition(leak)); + } + } + else + { + // Swimming outside the sub + character.AIController.SteeringManager.SteeringSeek(character.GetRelativeSimPosition(leak)); + } + } + else + { + // TODO: use the collider size? + if (!character.AnimController.InWater && character.AnimController is HumanoidAnimController && + Math.Abs(fromCharacterToLeak.X) < 100.0f && fromCharacterToLeak.Y < 0.0f && fromCharacterToLeak.Y > -150.0f) + { + ((HumanoidAnimController)character.AnimController).Crouching = true; + } + Vector2 standPos = new Vector2(Math.Sign(-fromCharacterToLeak.X), Math.Sign(-fromCharacterToLeak.Y)) / 2; if (leak.IsHorizontal) { standPos.X *= 2; @@ -468,43 +490,40 @@ namespace Barotrauma.Items.Components { standPos.X = 0; } - } - if (character.AIController.SteeringManager is IndoorsSteeringManager indoorSteering) - { - if (indoorSteering.CurrentPath != null && !indoorSteering.IsPathDirty && indoorSteering.CurrentPath.Unreachable) - { - Vector2 dir = Vector2.Normalize(standPos - character.WorldPosition); - character.AIController.SteeringManager.SteeringManual(deltaTime, dir / 2); - } - else - { - character.AIController.SteeringManager.SteeringSeek(standPos); - } - } - else - { character.AIController.SteeringManager.SteeringSeek(standPos); } } else { - if (dist < Range / 2) + if (dist < reach / 2) { // Too close -> steer away - character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.SimPosition - leak.SimPosition) / 2); + character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.SimPosition - leak.SimPosition)); } - else if (dist <= Range) + else if (dist <= reach) { // In range - character.AIController.SteeringManager.Reset(); - } - else - { - return false; + character.CursorPosition = leak.Position; + character.CursorPosition += VectorExtensions.Forward(Item.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2); + if (character.AnimController.InWater) + { + var torso = character.AnimController.GetLimb(LimbType.Torso); + // Turn facing the target when not moving (handled in the animcontroller if not moving) + Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition); + Vector2 diff = (mousePos - torso.SimPosition) * character.AnimController.Dir; + float newRotation = MathUtils.VectorToAngle(diff); + character.AnimController.Collider.SmoothRotate(newRotation, 5.0f); + + if (VectorExtensions.Angle(VectorExtensions.Forward(torso.body.TransformedRotation), fromCharacterToLeak) < MathHelper.PiOver4) + { + // Swim past + Vector2 moveDir = leak.IsHorizontal ? Vector2.UnitY : Vector2.UnitX; + moveDir *= character.AnimController.Dir; + character.AIController.SteeringManager.SteeringManual(deltaTime, moveDir); + } + } } } - sinTime += deltaTime; - character.CursorPosition = leak.Position + VectorExtensions.Forward(Item.body.TransformedRotation + (float)Math.Sin(sinTime), dist); if (item.RequireAimToUse) { bool isOperatingButtons = false; @@ -520,13 +539,33 @@ namespace Barotrauma.Items.Components { character.SetInput(InputType.Aim, false, true); } + bool isAiming = false; + var holdable = item.GetComponent(); + if (holdable != null) + { + isAiming = holdable.ControlPose; + } + sinTime = isAiming ? sinTime + deltaTime * 5 : 0; } // Press the trigger only when the tool is approximately facing the target. + Vector2 fromItemToLeak = leak.WorldPosition - item.WorldPosition; var angle = VectorExtensions.Angle(VectorExtensions.Forward(item.body.TransformedRotation), fromItemToLeak); if (angle < MathHelper.PiOver4) { - character.SetInput(InputType.Shoot, false, true); - Use(deltaTime, character); + // Check that we don't hit any friendlies + if (Submarine.PickBodies(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionCharacter).None(hit => + { + if (hit.UserData is Character c) + { + if (c == character) { return false; } + return HumanAIController.IsFriendly(character, c); + } + return false; + })) + { + character.SetInput(InputType.Shoot, false, true); + Use(deltaTime, character); + } } bool leakFixed = (leak.Open <= 0.0f || leak.Removed) && @@ -534,7 +573,6 @@ namespace Barotrauma.Items.Components if (leakFixed && leak.FlowTargetHull != null) { - sinTime = 0; if (!leak.FlowTargetHull.ConnectedGaps.Any(g => !g.IsRoomToRoom && g.Open > 0.0f)) { diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/ItemComponent.cs index 0bedaa73c..a7adf639f 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/ItemComponent.cs @@ -44,8 +44,9 @@ namespace Barotrauma.Items.Components public bool WasUsed; public readonly Dictionary> statusEffectLists; - + public Dictionary> requiredItems; + public readonly List DisabledRequiredItems = new List(); public List requiredSkills; @@ -271,19 +272,7 @@ namespace Barotrauma.Items.Components break; case "requireditem": case "requireditems": - RelatedItem ri = RelatedItem.Load(subElement, item.Name); - if (ri != null) - { - if (!requiredItems.ContainsKey(ri.Type)) - { - requiredItems.Add(ri.Type, new List()); - } - requiredItems[ri.Type].Add(ri); - } - else - { - DebugConsole.ThrowError("Error in item config \"" + item.ConfigFile + "\" - component " + GetType().ToString() + " requires an item with no identifiers."); - } + SetRequiredItems(subElement); break; case "requiredskill": case "requiredskills": @@ -323,6 +312,34 @@ namespace Barotrauma.Items.Components } } + public void SetRequiredItems(XElement element) + { + bool returnEmpty = false; +#if CLIENT + returnEmpty = Screen.Selected == GameMain.SubEditorScreen; +#endif + RelatedItem ri = RelatedItem.Load(element, returnEmpty, item.Name); + if (ri != null) + { + if (ri.Identifiers.Length == 0) + { + DisabledRequiredItems.Add(ri); + } + else + { + if (!requiredItems.ContainsKey(ri.Type)) + { + requiredItems.Add(ri.Type, new List()); + } + requiredItems[ri.Type].Add(ri); + } + } + else + { + DebugConsole.ThrowError("Error in item config \"" + item.ConfigFile + "\" - component " + GetType().ToString() + " requires an item with no identifiers."); + } + } + public virtual void Move(Vector2 amount) { } /// a Character has picked the item @@ -762,6 +779,12 @@ namespace Barotrauma.Items.Components componentElement.Add(newElement); } } + foreach (RelatedItem ri in DisabledRequiredItems) + { + XElement newElement = new XElement("requireditem"); + ri.Save(newElement); + componentElement.Add(newElement); + } SerializableProperty.SerializeProperties(this, componentElement); @@ -783,12 +806,16 @@ namespace Barotrauma.Items.Components var prevRequiredItems = new Dictionary>(requiredItems); requiredItems.Clear(); + bool returnEmptyRequirements = false; +#if CLIENT + returnEmptyRequirements = Screen.Selected == GameMain.SubEditorScreen; +#endif foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "requireditem": - RelatedItem newRequiredItem = RelatedItem.Load(subElement, item.Name); + RelatedItem newRequiredItem = RelatedItem.Load(subElement, returnEmptyRequirements, item.Name); if (newRequiredItem == null) continue; var prevRequiredItem = prevRequiredItems.ContainsKey(newRequiredItem.Type) ? diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/ItemContainer.cs index dbd110d73..eca6c032a 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/ItemContainer.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Barotrauma.Extensions; namespace Barotrauma.Items.Components { @@ -54,20 +55,44 @@ namespace Barotrauma.Items.Components [Serialize(5, false, description: "How many inventory slots the inventory has per row.")] public int SlotsPerRow { get; set; } - public List ContainableItems { get; private set; } + private HashSet containableRestrictions = new HashSet(); + [Editable, Serialize("", true, description: "Define items (by identifiers or tags) that bots should place inside this container. If empty, no restrictions are applied.")] + public string ContainableRestrictions + { + get { return string.Join(",", containableRestrictions); } + set + { + StringFormatter.ParseCommaSeparatedStringToCollection(value, containableRestrictions); + } + } + + public bool ShouldBeContained(string[] identifiersOrTags, out bool isRestrictionsDefined) + { + isRestrictionsDefined = containableRestrictions.Any(); + if (!isRestrictionsDefined) { return true; } + return identifiersOrTags.Any(id => containableRestrictions.Any(r => r == id)); + } + + public bool ShouldBeContained(Item item, out bool isRestrictionsDefined) + { + isRestrictionsDefined = containableRestrictions.Any(); + if (!isRestrictionsDefined) { return true; } + return containableRestrictions.Any(id => item.Prefab.Identifier == id || item.HasTag(id)); + } + + public List ContainableItems { get; private set; } = new List(); public ItemContainer(Item item, XElement element) : base (item, element) { - Inventory = new ItemInventory(item, this, capacity, SlotsPerRow); - ContainableItems = new List(); + Inventory = new ItemInventory(item, this, capacity, SlotsPerRow); foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "containable": - RelatedItem containable = RelatedItem.Load(subElement, item.Name); + RelatedItem containable = RelatedItem.Load(subElement, returnEmpty: false, parentDebugName: item.Name); if (containable == null) { DebugConsole.ThrowError("Error in item config \"" + item.ConfigFile + "\" - containable with no identifiers."); diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Deconstructor.cs index 4c0291752..677f6aac8 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Deconstructor.cs @@ -34,6 +34,7 @@ namespace Barotrauma.Items.Components public override void OnItemLoaded() { + base.OnItemLoaded(); var containers = item.GetComponents().ToList(); if (containers.Count < 2) { @@ -59,7 +60,7 @@ namespace Barotrauma.Items.Components return; } - hasPower = voltage >= minVoltage; + hasPower = Voltage >= MinVoltage; if (!hasPower) { return; } var repairable = item.GetComponent(); @@ -70,10 +71,8 @@ namespace Barotrauma.Items.Components ApplyStatusEffects(ActionType.OnActive, deltaTime, null); - if (powerConsumption == 0.0f) { voltage = 1.0f; } - - progressTimer += deltaTime * voltage; - Voltage -= deltaTime * 10.0f; + if (powerConsumption <= 0.0f) { Voltage = 1.0f; } + progressTimer += deltaTime * Voltage; var targetItem = inputContainer.Inventory.Items.LastOrDefault(i => i != null); if (targetItem == null) { return; } @@ -99,7 +98,7 @@ namespace Barotrauma.Items.Components float condition = deconstructProduct.CopyCondition ? percentageHealth * itemPrefab.Health : itemPrefab.Health * deconstructProduct.OutCondition; - + //container full, drop the items outside the deconstructor if (emptySlots <= 0) { @@ -111,7 +110,7 @@ namespace Barotrauma.Items.Components emptySlots--; } } - + if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { if (targetItem.Prefab.DeconstructItems.Any()) @@ -149,8 +148,6 @@ namespace Barotrauma.Items.Components progressState = 0.0f; } } - - voltage -= deltaTime * 10.0f; } private void PutItemsToLinkedContainer() diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Engine.cs index 2f3d61d1a..5de4f1f7f 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Engine.cs @@ -50,7 +50,7 @@ namespace Barotrauma.Items.Components public float CurrentVolume { - get { return Math.Abs((force / 100.0f) * (minVoltage <= 0.0f ? 1.0f : Math.Min(prevVoltage / minVoltage, 1.0f))); } + get { return Math.Abs((force / 100.0f) * (MinVoltage <= 0.0f ? 1.0f : Math.Min(prevVoltage / MinVoltage, 1.0f))); } } public Engine(Item item, XElement element) @@ -83,15 +83,15 @@ namespace Barotrauma.Items.Components //pumps consume more power when in a bad condition currPowerConsumption *= MathHelper.Lerp(2.0f, 1.0f, item.Condition / item.MaxCondition); - if (powerConsumption == 0.0f) voltage = 1.0f; + if (powerConsumption == 0.0f) { Voltage = 1.0f; } - prevVoltage = voltage; - hasPower = voltage > minVoltage; + prevVoltage = Voltage; + hasPower = Voltage > MinVoltage; - Force = MathHelper.Lerp(force, (voltage < minVoltage) ? 0.0f : targetForce, 0.1f); + Force = MathHelper.Lerp(force, (Voltage < MinVoltage) ? 0.0f : targetForce, 0.1f); if (Math.Abs(Force) > 1.0f) { - Vector2 currForce = new Vector2((force / 10.0f) * maxForce * Math.Min(voltage / minVoltage, 1.0f), 0.0f); + Vector2 currForce = new Vector2((force / 10.0f) * maxForce * Math.Min(Voltage / MinVoltage, 1.0f), 0.0f); //less effective when in a bad condition currForce *= MathHelper.Lerp(0.5f, 2.0f, item.Condition / item.MaxCondition); @@ -119,8 +119,6 @@ namespace Barotrauma.Items.Components } #endif } - - voltage -= deltaTime; } private void UpdatePropellerDamage(float deltaTime) @@ -172,5 +170,16 @@ namespace Barotrauma.Items.Components } } } + + public override XElement Save(XElement parentElement) + { + Vector2 prevPropellerPos = PropellerPos; + //undo flipping before saving + if (item.FlippedX) { PropellerPos = new Vector2(-PropellerPos.X, PropellerPos.Y); } + if (item.FlippedY) { PropellerPos = new Vector2(PropellerPos.X, -PropellerPos.Y); } + XElement element = base.Save(parentElement); + PropellerPos = prevPropellerPos; + return element; + } } } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Fabricator.cs index 73e41e196..01a21467b 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Fabricator.cs @@ -70,6 +70,7 @@ namespace Barotrauma.Items.Components public override void OnItemLoaded() { + base.OnItemLoaded(); var containers = item.GetComponents().ToList(); if (containers.Count < 2) { @@ -199,7 +200,7 @@ namespace Barotrauma.Items.Components progressState = fabricatedItem == null ? 0.0f : (requiredTime - timeUntilReady) / requiredTime; - hasPower = voltage >= minVoltage; + hasPower = Voltage >= MinVoltage; if (!hasPower) { return; } var repairable = item.GetComponent(); @@ -210,10 +211,9 @@ namespace Barotrauma.Items.Components ApplyStatusEffects(ActionType.OnActive, deltaTime, null); - if (powerConsumption <= 0) { voltage = 1.0f; } + if (powerConsumption <= 0) { Voltage = 1.0f; } - timeUntilReady -= deltaTime * voltage; - voltage -= deltaTime * 10.0f; + timeUntilReady -= deltaTime * Voltage; if (timeUntilReady > 0.0f) { return; } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/MiniMap.cs index 627e7b1b1..667793f6e 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/MiniMap.cs @@ -75,13 +75,11 @@ namespace Barotrauma.Items.Components currPowerConsumption = powerConsumption; currPowerConsumption *= MathHelper.Lerp(2.0f, 1.0f, item.Condition / item.MaxCondition); - hasPower = voltage > minVoltage; + hasPower = Voltage > MinVoltage; if (hasPower) { ApplyStatusEffects(ActionType.OnActive, deltaTime, null); } - - voltage -= deltaTime; } public override bool Pick(Character picker) diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/OxygenGenerator.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/OxygenGenerator.cs index 702ee23cb..3334b018c 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/OxygenGenerator.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/OxygenGenerator.cs @@ -46,12 +46,12 @@ namespace Barotrauma.Items.Components if (powerConsumption <= 0.0f) { - voltage = 1.0f; + Voltage = 1.0f; } if (item.CurrentHull == null) return; - if (voltage < minVoltage) + if (Voltage < MinVoltage) { powerDownTimer += deltaTime; return; @@ -61,7 +61,7 @@ namespace Barotrauma.Items.Components powerDownTimer = 0.0f; } - CurrFlow = Math.Min(voltage, 1.0f) * generatedAmount * 100.0f; + CurrFlow = Math.Min(Voltage, 1.0f) * generatedAmount * 100.0f; //less effective when in bad condition float conditionMult = item.Condition / item.MaxCondition; @@ -71,8 +71,6 @@ namespace Barotrauma.Items.Components CurrFlow *= conditionMult * conditionMult; UpdateVents(CurrFlow); - - voltage -= deltaTime; } public override void UpdateBroken(float deltaTime, Camera cam) diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Pump.cs index 5bf8dcc7f..862b767c1 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Pump.cs @@ -76,7 +76,7 @@ namespace Barotrauma.Items.Components //pumps consume more power when in a bad condition currPowerConsumption *= MathHelper.Lerp(2.0f, 1.0f, item.Condition / item.MaxCondition); - if (voltage < minVoltage) { return; } + if (Voltage < MinVoltage) { return; } UpdateProjSpecific(deltaTime); @@ -86,7 +86,7 @@ namespace Barotrauma.Items.Components if (item.CurrentHull == null) { return; } - float powerFactor = currPowerConsumption <= 0.0f ? 1.0f : voltage; + float powerFactor = currPowerConsumption <= 0.0f ? 1.0f : Voltage; currFlow = flowPercentage / 100.0f * maxFlow * powerFactor; //less effective when in a bad condition @@ -94,8 +94,6 @@ namespace Barotrauma.Items.Components item.CurrentHull.WaterVolume += currFlow; if (item.CurrentHull.WaterVolume > item.CurrentHull.Volume) { item.CurrentHull.Pressure += 0.5f; } - - voltage -= deltaTime; } partial void UpdateProjSpecific(float deltaTime); @@ -124,7 +122,7 @@ namespace Barotrauma.Items.Components { if (float.TryParse(signal, NumberStyles.Any, CultureInfo.InvariantCulture, out float tempTarget)) { - targetLevel = MathHelper.Clamp((tempTarget + 100.0f) / 2.0f, 0.0f, 100.0f); + targetLevel = MathHelper.Clamp(tempTarget + 50.0f, 0.0f, 100.0f); controlLockTimer = 0.1f; } } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Reactor.cs index 76879e86e..351ff33c9 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Reactor.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Barotrauma.Extensions; namespace Barotrauma.Items.Components { @@ -35,6 +36,7 @@ namespace Barotrauma.Items.Components private float maxPowerOutput; + private Queue loadQueue = new Queue(); private float load; private bool unsentChanges; @@ -165,7 +167,10 @@ namespace Barotrauma.Items.Components private float prevAvailableFuel; public float AvailableFuel { get; set; } - + + private readonly string[] fuelTags = new string[1] { "reactorfuel" }; + + public Reactor(Item item, XElement element) : base(item, element) { @@ -267,8 +272,7 @@ namespace Barotrauma.Items.Components { UpdateAutoTemp(2.0f, deltaTime); } - - load = 0.0f; + float currentLoad = 0.0f; List connections = item.Connections; if (connections != null && connections.Count > 0) { @@ -284,13 +288,20 @@ namespace Barotrauma.Items.Components //calculate how much external power there is in the grid //(power coming from somewhere else than this reactor, e.g. batteries) - float externalPower = Math.Max(CurrPowerConsumption - pt.CurrPowerConsumption, 0); + float externalPower = Math.Max(CurrPowerConsumption - pt.CurrPowerConsumption, 0) * 0.95f; //reduce the external power from the load to prevent overloading the grid - load = Math.Max(load, pt.PowerLoad - externalPower); + currentLoad = Math.Max(currentLoad, pt.PowerLoad - externalPower); } } } + loadQueue.Enqueue(currentLoad); + while (loadQueue.Count() > 60.0f) + { + load = loadQueue.Average(); + loadQueue.Dequeue(); + } + if (fissionRate > 0.0f) { foreach (Item item in item.ContainedItems) @@ -505,6 +516,19 @@ namespace Barotrauma.Items.Components return picker != null; } + private int itemIndex; + private List ignoredContainers = new List(); + private bool FindSuitableContainer(Character character, Func priority, out Item suitableContainer) + { + suitableContainer = null; + if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: ignoredContainers, customPriorityFunction: priority)) + { + suitableContainer = targetContainer; + return true; + } + return false; + } + public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; } @@ -516,13 +540,56 @@ namespace Barotrauma.Items.Components //characters with insufficient skill levels don't refuel the reactor if (degreeOfSuccess > 0.2f) { - //remove used-up fuel from the reactor - var containedItems = item.ContainedItems; - foreach (Item item in containedItems) + if (objective.SubObjectives.None()) { - if (item != null && item.Condition <= 0.0f) + var containedItems = item.ContainedItems; + foreach (Item fuelRod in containedItems) { - item.Drop(character); + if (fuelRod != null && fuelRod.Condition <= 0.0f) + { + if (!FindSuitableContainer(character, + i => + { + var container = i.GetComponent(); + if (container == null) { return 0; } + if (container.Inventory.IsFull()) { return 0; } + if (container.ShouldBeContained(fuelRod, out bool isRestrictionsDefined)) + { + if (isRestrictionsDefined) + { + return 3; + } + else + { + if (fuelRod.Prefab.IsContainerPreferred(container, out bool isPreferencesDefined)) + { + return isPreferencesDefined ? 2 : 1; + } + else + { + return isPreferencesDefined ? 0 : 1; + } + } + } + else + { + return 0; + } + }, out Item targetContainer)) + { + return false; + } + var decontainObjective = new AIObjectiveDecontainItem(character, fuelRod, item.GetComponent(), objective.objectiveManager, targetContainer?.GetComponent()); + decontainObjective.Abandoned += () => + { + itemIndex = 0; + if (targetContainer != null) + { + ignoredContainers.Add(targetContainer); + } + }; + objective.AddSubObjectiveInQueue(decontainObjective); + } } } @@ -535,31 +602,33 @@ namespace Barotrauma.Items.Components //load more fuel if the current maximum output is only 50% of the current load if (NeedMoreFuel(minimumOutputRatio: 0.5f)) { - var containFuelObjective = new AIObjectiveContainItem(character, new string[] { "fuelrod", "reactorfuel" }, item.GetComponent(), objective.objectiveManager) - { - targetItemCount = item.ContainedItems.Count(i => i != null && i.Prefab.Identifier == "fuelrod" || i.HasTag("reactorfuel")) + 1, - GetItemPriority = (Item fuelItem) => - { - if (fuelItem.ParentInventory?.Owner is Item) - { - //don't take fuel from other reactors - if (((Item)fuelItem.ParentInventory.Owner).GetComponent() != null) return 0.0f; - } - return 1.0f; - } - }; - objective.AddSubObjective(containFuelObjective); - - character?.Speak(TextManager.Get("DialogReactorFuel"), null, 0.0f, "reactorfuel", 30.0f); - aiUpdateTimer = AIUpdateInterval; + if (objective.SubObjectives.None()) + { + var containFuelObjective = new AIObjectiveContainItem(character, fuelTags, item.GetComponent(), objective.objectiveManager) + { + targetItemCount = item.ContainedItems.Count(i => i != null && fuelTags.Any(t => i.Prefab.Identifier == t || i.HasTag(t))) + 1, + GetItemPriority = (Item fuelItem) => + { + if (fuelItem.ParentInventory?.Owner is Item) + { + //don't take fuel from other reactors + if (((Item)fuelItem.ParentInventory.Owner).GetComponent() != null) return 0.0f; + } + return 1.0f; + } + }; + containFuelObjective.Abandoned += () => objective.Abandon = true; + objective.AddSubObjective(containFuelObjective); + character?.Speak(TextManager.Get("DialogReactorFuel"), null, 0.0f, "reactorfuel", 30.0f); + } return false; } else if (TooMuchFuel()) { foreach (Item item in item.ContainedItems) { - if (item != null && item.HasTag("reactorfuel")) + if (item != null && fuelTags.Any(t => item.Prefab.Identifier == t || item.HasTag(t))) { if (!character.Inventory.TryPutItem(item, character, allowedSlots: item.AllowedSlots)) { @@ -577,22 +646,28 @@ namespace Barotrauma.Items.Components } LastUser = lastAIUser = character; - + + bool prevAutoTemp = autoTemp; + bool prevShutDown = shutDown; + float prevFissionRate = targetFissionRate; + float prevTurbineOutput = targetTurbineOutput; + switch (objective.Option.ToLowerInvariant()) { case "powerup": shutDown = false; - //characters with insufficient skill levels simply set the autotemp on instead of trying to adjust the temperature manually - if (degreeOfSuccess < 0.5f) + if (objective.Override || !autoTemp) { - if (!autoTemp) unsentChanges = true; - AutoTemp = true; - } - else - { - AutoTemp = false; - unsentChanges = true; - UpdateAutoTemp(MathHelper.Lerp(0.5f, 2.0f, degreeOfSuccess), 1.0f); + //characters with insufficient skill levels simply set the autotemp on instead of trying to adjust the temperature manually + if (degreeOfSuccess < 0.5f) + { + AutoTemp = true; + } + else + { + AutoTemp = false; + UpdateAutoTemp(MathHelper.Lerp(0.5f, 2.0f, degreeOfSuccess), 1.0f); + } } #if CLIENT onOffSwitch.BarScroll = 0.0f; @@ -604,11 +679,6 @@ namespace Barotrauma.Items.Components #if CLIENT onOffSwitch.BarScroll = 1.0f; #endif - if (AutoTemp || !shutDown || targetFissionRate > 0.0f || targetTurbineOutput > 0.0f) - { - unsentChanges = true; - } - AutoTemp = false; shutDown = true; targetFissionRate = 0.0f; @@ -616,6 +686,14 @@ namespace Barotrauma.Items.Components break; } + if (autoTemp != prevAutoTemp || + prevShutDown != shutDown || + Math.Abs(prevFissionRate - targetFissionRate) > 1.0f || + Math.Abs(prevTurbineOutput - targetTurbineOutput) > 1.0f) + { + unsentChanges = true; + } + aiUpdateTimer = AIUpdateInterval; return false; diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Sonar.cs index e349fa50e..4c45431d0 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Sonar.cs @@ -162,7 +162,7 @@ namespace Barotrauma.Items.Components if (currentMode == Mode.Active) { - if ((voltage >= minVoltage || powerConsumption <= 0.0f) && + if ((Voltage >= MinVoltage) && (!UseTransducers || connectedTransducers.Count > 0)) { if (currentPingIndex != -1) @@ -201,7 +201,6 @@ namespace Barotrauma.Items.Components { item.AiTarget.SectorDegrees = 360.0f; } - currentPingIndex = -1; aiPingCheckPending = false; } } @@ -235,6 +234,7 @@ namespace Barotrauma.Items.Components protected override void RemoveComponentSpecific() { + base.RemoveComponentSpecific(); sonarBlip?.Remove(); pingCircle?.Remove(); directionalPingCircle?.Remove(); @@ -247,6 +247,7 @@ namespace Barotrauma.Items.Components { if (currentMode == Mode.Passive || !aiPingCheckPending) return false; + // TODO: Don't create new collections here Dictionary> targetGroups = new Dictionary>(); foreach (Character c in Character.CharacterList) diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/SonarTransducer.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/SonarTransducer.cs index 927ebbf9c..58a0799d3 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/SonarTransducer.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/SonarTransducer.cs @@ -20,7 +20,7 @@ namespace Barotrauma.Items.Components { UpdateOnActiveEffects(deltaTime); - if (voltage >= minVoltage || PowerConsumption <= 0.0f) + if (Voltage >= MinVoltage) { sendSignalTimer += deltaTime; if (sendSignalTimer > SendSignalInterval) @@ -29,8 +29,6 @@ namespace Barotrauma.Items.Components sendSignalTimer = SendSignalInterval; } } - - voltage = 0.0f; } } } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Steering.cs index 2c2411e1e..098478821 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Steering.cs @@ -15,6 +15,13 @@ namespace Barotrauma.Items.Components private const float AutopilotRayCastInterval = 0.5f; private const float RecalculatePathInterval = 5.0f; + private const float AutopilotMinDistToPathNode = 30.0f; + + private const float AutoPilotSteeringLerp = 0.1f; + + private const float AutoPilotMaxSpeed = 0.5f; + private const float AIPilotMaxSpeed = 1.0f; + private Vector2 currVelocity; private Vector2 targetVelocity; @@ -162,6 +169,7 @@ namespace Barotrauma.Items.Components public override void OnItemLoaded() { + base.OnItemLoaded(); sonar = item.GetComponent(); } @@ -209,7 +217,7 @@ namespace Barotrauma.Items.Components currPowerConsumption = powerConsumption; - if (voltage < minVoltage && currPowerConsumption > 0.0f) { return; } + if (Voltage < MinVoltage) { return; } if (user != null && user.Removed) { @@ -221,6 +229,12 @@ namespace Barotrauma.Items.Components if (autoPilot) { UpdateAutoPilot(deltaTime); + float userSkill = 0.0f; + if (user != null && (user.SelectedConstruction == item || item.linkedTo.Contains(user.SelectedConstruction))) + { + userSkill = user.GetSkillLevel("helm") / 100.0f; + } + targetVelocity = targetVelocity.ClampLength(MathHelper.Lerp(AutoPilotMaxSpeed, AIPilotMaxSpeed, userSkill) * 100.0f); } else { @@ -253,8 +267,6 @@ namespace Barotrauma.Items.Components targetLevel += (neutralBallastLevel - 0.5f) * 100.0f; item.SendSignal(0, targetLevel.ToString(CultureInfo.InvariantCulture), "velocity_y_out", null); - - voltage -= deltaTime; } private void UpdateAutoPilot(float deltaTime) @@ -262,7 +274,8 @@ namespace Barotrauma.Items.Components if (controlledSub == null) return; if (posToMaintain != null) { - SteerTowardsPosition((Vector2)posToMaintain); + Vector2 steeringVel = GetSteeringVelocity((Vector2)posToMaintain); + TargetVelocity = Vector2.Lerp(TargetVelocity, steeringVel, AutoPilotSteeringLerp); return; } @@ -284,7 +297,7 @@ namespace Barotrauma.Items.Components //if the node is close enough, check if it's visible float lengthSqr = diff.LengthSquared(); - if (lengthSqr > 0.001f && lengthSqr < 500.0f) + if (lengthSqr > 0.001f && lengthSqr < AutopilotMinDistToPathNode * AutopilotMinDistToPathNode) { diff = Vector2.Normalize(diff); @@ -298,11 +311,11 @@ namespace Barotrauma.Items.Components Vector2 cornerPos = new Vector2(controlledSub.Borders.Width * x, controlledSub.Borders.Height * y) / 2.0f; - cornerPos = ConvertUnits.ToSimUnits(cornerPos * 1.2f + controlledSub.WorldPosition); + cornerPos = ConvertUnits.ToSimUnits(cornerPos * 1.1f + controlledSub.WorldPosition); float dist = Vector2.Distance(cornerPos, steeringPath.NextNode.SimPosition); - if (Submarine.PickBody(cornerPos, cornerPos + diff * dist, null, Physics.CollisionLevel) == null) continue; + if (Submarine.PickBody(cornerPos, cornerPos + diff * dist, null, Physics.CollisionLevel) == null) { continue; } nextVisible = false; x = 2; @@ -313,19 +326,18 @@ namespace Barotrauma.Items.Components if (nextVisible) steeringPath.SkipToNextNode(); } - - autopilotRayCastTimer = AutopilotRayCastInterval; } + Vector2 newVelocity = Vector2.Zero; if (steeringPath.CurrentNode != null) { - SteerTowardsPosition(steeringPath.CurrentNode.WorldPosition); + newVelocity = GetSteeringVelocity(steeringPath.CurrentNode.WorldPosition); } Vector2 avoidDist = new Vector2( - Math.Max(1000.0f * Math.Abs(controlledSub.Velocity.X), controlledSub.Borders.Width * 1.5f), - Math.Max(1000.0f * Math.Abs(controlledSub.Velocity.Y), controlledSub.Borders.Height * 1.5f)); + Math.Max(1000.0f * Math.Abs(controlledSub.Velocity.X), controlledSub.Borders.Width * 0.75f), + Math.Max(1000.0f * Math.Abs(controlledSub.Velocity.Y), controlledSub.Borders.Height * 0.75f)); float avoidRadius = avoidDist.Length(); @@ -356,22 +368,22 @@ namespace Barotrauma.Items.Components 0.0f : Vector2.Dot(controlledSub.Velocity, -normalizedDiff); //not heading towards the wall -> ignore - if (dot < 0.5) + if (dot < 1.0) { debugDrawObstacles.Add(new ObstacleDebugInfo(edge, intersection, dot, Vector2.Zero, cell.Translation)); continue; } - Vector2 change = (normalizedDiff * Math.Max((avoidRadius - diff.Length()), 0.0f)) / avoidRadius; - newAvoidStrength += change * dot; - debugDrawObstacles.Add(new ObstacleDebugInfo(edge, intersection, dot, change * dot, cell.Translation)); + Vector2 change = (normalizedDiff * Math.Max((avoidRadius - diff.Length()), 0.0f)) / avoidRadius; + if (change.LengthSquared() < 0.001f) { continue; } + newAvoidStrength += change * (dot - 1.0f); + debugDrawObstacles.Add(new ObstacleDebugInfo(edge, intersection, dot - 1.0f, change * (dot - 1.0f), cell.Translation)); } } } avoidStrength = Vector2.Lerp(avoidStrength, newAvoidStrength, deltaTime * 10.0f); - - targetVelocity += avoidStrength * 100.0f; + TargetVelocity = Vector2.Lerp(TargetVelocity, newVelocity + avoidStrength * 100.0f, AutoPilotSteeringLerp); //steer away from other subs foreach (Submarine sub in Submarine.Loaded) @@ -447,21 +459,21 @@ namespace Barotrauma.Items.Components UpdatePath(); } } - private void SteerTowardsPosition(Vector2 worldPosition) + private Vector2 GetSteeringVelocity(Vector2 worldPosition) { - float prediction = 10.0f; + float prediction = 2.0f; Vector2 futurePosition = ConvertUnits.ToDisplayUnits(controlledSub.Velocity) * prediction; Vector2 targetSpeed = ((worldPosition - controlledSub.WorldPosition) - futurePosition); - if (targetSpeed.Length() > 500.0f) + if (targetSpeed.LengthSquared() > 500.0f * 500.0f) { - targetSpeed = Vector2.Normalize(targetSpeed); - TargetVelocity = targetSpeed * 100.0f; + + return Vector2.Normalize(targetSpeed) * 100.0f; } else { - TargetVelocity = targetSpeed / 5.0f; + return targetSpeed / 5.0f; } } @@ -471,43 +483,53 @@ namespace Barotrauma.Items.Components { character.Speak(TextManager.Get("DialogSteeringTaken"), null, 0.0f, "steeringtaken", 10.0f); } - user = character; - + if (!AutoPilot) + { + unsentChanges = true; + AutoPilot = true; + } switch (objective.Option.ToLowerInvariant()) { case "maintainposition": - if (!posToMaintain.HasValue) + if (objective.Override) { - unsentChanges = true; - posToMaintain = controlledSub != null ? - controlledSub.WorldPosition : - item.Submarine == null ? item.WorldPosition : item.Submarine.WorldPosition; + if (!MaintainPos) + { + unsentChanges = true; + MaintainPos = true; + } + if (!posToMaintain.HasValue) + { + unsentChanges = true; + posToMaintain = controlledSub != null ? + controlledSub.WorldPosition : + item.Submarine == null ? item.WorldPosition : item.Submarine.WorldPosition; + } } - - if (!AutoPilot || !MaintainPos) unsentChanges = true; - - AutoPilot = true; - MaintainPos = true; break; case "navigateback": - if (!AutoPilot || MaintainPos || LevelEndSelected || !LevelStartSelected) + if (objective.Override) { - unsentChanges = true; + if (MaintainPos || LevelEndSelected || !LevelStartSelected) + { + unsentChanges = true; + } + SetDestinationLevelStart(); } - SetDestinationLevelStart(); break; case "navigatetodestination": - if (!AutoPilot || MaintainPos || !LevelEndSelected || LevelStartSelected) + if (objective.Override) { - unsentChanges = true; + if (MaintainPos || !LevelEndSelected || LevelStartSelected) + { + unsentChanges = true; + } + SetDestinationLevelEnd(); } - SetDestinationLevelEnd(); break; } - sonar?.AIOperate(deltaTime, character, objective); - return false; } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Power/PowerContainer.cs index 55e4241d8..38487261f 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Power/PowerContainer.cs @@ -14,7 +14,7 @@ namespace Barotrauma.Items.Components private float charge; - private float rechargeVoltage; + //private float rechargeVoltage; //how fast the battery can be recharged private float maxRechargeSpeed; @@ -28,10 +28,7 @@ namespace Barotrauma.Items.Components protected Vector2 indicatorPosition, indicatorSize; protected bool isHorizontal; - - //a list of powered devices connected directly to this item - private readonly List> directlyConnected = new List>(10); - + public float CurrPowerOutput { get; @@ -107,12 +104,18 @@ namespace Barotrauma.Items.Components if (!MathUtils.IsValid(value)) return; rechargeSpeed = MathHelper.Clamp(value, 0.0f, maxRechargeSpeed); rechargeSpeed = MathUtils.RoundTowardsClosest(rechargeSpeed, Math.Max(maxRechargeSpeed * 0.1f, 1.0f)); + if (isRunning) + { + HasBeenTuned = true; + } } } public float RechargeRatio => RechargeSpeed / MaxRechargeSpeed; public const float aiRechargeTargetRatio = 0.5f; + private bool isRunning; + public bool HasBeenTuned { get; private set; } public PowerContainer(Item item, XElement element) : base(item, element) @@ -131,14 +134,13 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { + isRunning = true; float chargeRatio = charge / capacity; float gridPower = 0.0f; float gridLoad = 0.0f; - directlyConnected.Clear(); - foreach (Connection c in item.Connections) { - if (c.Name == "power_in") continue; + if (!c.IsPower || !c.IsOutput) { continue; } foreach (Connection c2 in c.Recipients) { if (c2.Item.Condition <= 0.0f) { continue; } @@ -149,15 +151,13 @@ namespace Barotrauma.Items.Components foreach (Powered powered in c2.Item.GetComponents()) { if (!powered.IsActive) continue; - directlyConnected.Add(new Pair(powered, c2)); gridLoad += powered.CurrPowerConsumption; } continue; } if (!pt.IsActive || !pt.CanTransfer) { continue; } - - gridLoad += pt.PowerLoad; gridPower -= pt.CurrPowerConsumption; + gridLoad += pt.PowerLoad; } } @@ -168,66 +168,51 @@ namespace Barotrauma.Items.Components if (charge >= capacity) { - rechargeVoltage = 0.0f; + //rechargeVoltage = 0.0f; charge = capacity; - CurrPowerConsumption = 0.0f; } else { currPowerConsumption = MathHelper.Lerp(currPowerConsumption, rechargeSpeed, 0.05f); - Charge += currPowerConsumption * rechargeVoltage / 3600.0f; + Charge += currPowerConsumption * Voltage / 3600.0f; } - - //provide power to the grid - if (gridLoad > 0.0f) + + + if (charge <= 0.0f) { - if (charge <= 0.0f) - { - CurrPowerOutput = 0.0f; - charge = 0.0f; - return; - } - - if (gridPower < gridLoad) - { - //output starts dropping when the charge is less than 10% - float maxOutputRatio = 1.0f; - if (chargeRatio < 0.1f) - { - maxOutputRatio = Math.Max(chargeRatio * 10.0f, 0.0f); - } - - CurrPowerOutput = MathHelper.Lerp( - CurrPowerOutput, - Math.Min(MaxOutPut * maxOutputRatio, gridLoad), - deltaTime * 10.0f); - } - else - { - CurrPowerOutput = MathHelper.Lerp(CurrPowerOutput, 0.0f, deltaTime * 10.0f); - } - - Charge -= CurrPowerOutput / 3600.0f; + CurrPowerOutput = 0.0f; + charge = 0.0f; + return; } - item.SendSignal(0, ((int)Charge).ToString(), "charge", null); - item.SendSignal(0, ((int)((Charge / capacity) * 100)).ToString(), "charge_%", null); - item.SendSignal(0, ((int)((RechargeSpeed / maxRechargeSpeed) * 100)).ToString(), "charge_rate", null); - foreach (Pair connected in directlyConnected) + //output starts dropping when the charge is less than 10% + float maxOutputRatio = 1.0f; + if (chargeRatio < 0.1f) { - connected.First.ReceiveSignal(0, "", connected.Second, source: item, sender: null, - power: gridLoad <= 0.0f ? 1.0f : CurrPowerOutput / gridLoad); + maxOutputRatio = Math.Max(chargeRatio * 10.0f, 0.0f); } - rechargeVoltage = 0.0f; + CurrPowerOutput += (gridLoad - gridPower) * deltaTime; + + float maxOutput = Math.Min(MaxOutPut * maxOutputRatio, gridLoad); + CurrPowerOutput = MathHelper.Clamp(CurrPowerOutput, 0.0f, maxOutput); + Charge -= CurrPowerOutput / 3600.0f; + + item.SendSignal(0, ((int)Math.Round(Charge)).ToString(), "charge", null); + item.SendSignal(0, ((int)Math.Round((Charge / capacity) * 100)).ToString(), "charge_%", null); + item.SendSignal(0, ((int)Math.Round((RechargeSpeed / maxRechargeSpeed) * 100)).ToString(), "charge_rate", null); } public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { -#if CLIENT - if (GameMain.Client != null) return false; -#endif + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; } + + if (objective.Override) + { + HasBeenTuned = false; + } + if (HasBeenTuned) { return true; } if (string.IsNullOrEmpty(objective.Option) || objective.Option.ToLowerInvariant() == "charge") { @@ -274,6 +259,8 @@ namespace Barotrauma.Items.Components public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power, float signalStrength = 1.0f) { + if (connection.IsPower) { return; } + if (connection.Name == "set_rate") { if (float.TryParse(signal, NumberStyles.Any, CultureInfo.InvariantCulture, out float tempSpeed)) @@ -290,12 +277,6 @@ namespace Barotrauma.Items.Components #endif } } - if (!connection.IsPower) { return; } - - if (connection.Name == "power_in") - { - rechargeVoltage = Math.Min(power, 1.0f); - } } } } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Power/PowerTransfer.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Power/PowerTransfer.cs index 8713a7ef6..7d6e347ee 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Power/PowerTransfer.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Power/PowerTransfer.cs @@ -8,37 +8,21 @@ namespace Barotrauma.Items.Components { partial class PowerTransfer : Powered { - private static float fullPower; - private static float fullLoad; + public List PowerConnections { get; private set; } - private int updateCount; - - //affects how fast changes in power/load are carried over the grid - static float inertia = 5.0f; - - private static HashSet connectedList = new HashSet(); - private List powerConnections; - public List PowerConnections - { - get - { - return powerConnections; - } - } - - - private Dictionary connectionDirty = new Dictionary(); + private readonly Dictionary connectionDirty = new Dictionary(); //a list of connections a given connection is connected to, either directly or via other power transfer components - private Dictionary> connectedRecipients = new Dictionary>(); + private readonly Dictionary> connectedRecipients = new Dictionary>(); - private float powerLoad; + protected float powerLoad; - private bool isBroken; + protected bool isBroken; public float PowerLoad { get { return powerLoad; } + set { powerLoad = value; } } [Editable, Serialize(true, true, description: "Can the item be damaged if too much power is supplied to the power grid.")] @@ -145,97 +129,43 @@ namespace Barotrauma.Items.Components SetAllConnectionsDirty(); isBroken = false; } - - if (updateCount > 0) + + ApplyStatusEffects(ActionType.OnActive, deltaTime, null); + + //if the item can't be fixed, don't allow it to break + if (!item.Repairables.Any() || !CanBeOverloaded) { return; } + + float maxOverVoltage = Math.Max(OverloadVoltage, 1.0f); + Overload = -currPowerConsumption > Math.Max(powerLoad, 200.0f) * maxOverVoltage; + if (Overload && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)) { - //this junction box has already been updated this frame - updateCount--; - return; - } + //damage the item if voltage is too high (except if running as a client) + float prevCondition = item.Condition; + item.Condition -= deltaTime * 10.0f; - Overload = false; - - //reset and recalculate the power generated/consumed - //by the constructions connected to the grid - fullPower = 0.0f; - fullLoad = 0.0f; - - connectedList.Clear(); - - updateCount = 0; - CheckJunctions(deltaTime); - - foreach (Powered p in connectedList) - { - PowerTransfer pt = p as PowerTransfer; - if (pt == null || pt.updateCount == 0) { continue; } - - if (pt is RelayComponent != this is RelayComponent) { continue; } - - pt.Overload = false; - pt.powerLoad += (fullLoad - pt.powerLoad) / inertia; - pt.currPowerConsumption += (-fullPower - pt.currPowerConsumption) / inertia; - - float voltage = fullPower / Math.Max(fullLoad, 1.0f); - if (this is RelayComponent) - { - pt.currPowerConsumption = Math.Max(-fullLoad, pt.currPowerConsumption); - voltage = Math.Min(voltage, 1.0f); - } - - pt.Item.SendSignal(0, "", "power", null, voltage); - pt.Item.SendSignal(0, "", "power_out", null, voltage); - - //items in a bad condition are more sensitive to overvoltage - float maxOverVoltage = MathHelper.Lerp(OverloadVoltage * 0.75f, OverloadVoltage, pt.item.Condition / pt.item.MaxCondition); - maxOverVoltage = Math.Max(OverloadVoltage, 1.0f); - - //if the item can't be fixed, don't allow it to break - if (!pt.item.Repairables.Any() || !pt.CanBeOverloaded) { continue; } - - //relays don't blow up if the power is higher than load, only if the output is high enough - //(i.e. enough power passing through the relay) - if (pt is RelayComponent) { continue; } - - if (-pt.currPowerConsumption < Math.Max(pt.powerLoad, 200.0f) * maxOverVoltage) { continue; } - - pt.Overload = true; -#if CLIENT - //damage the item if voltage is too high - //(except if running as a client) - if (GameMain.Client != null) { continue; } -#endif - float prevCondition = pt.item.Condition; - pt.item.Condition -= deltaTime * 10.0f; - - if (pt.item.Condition <= 0.0f && prevCondition > 0.0f) + if (item.Condition <= 0.0f && prevCondition > 0.0f) { #if CLIENT - SoundPlayer.PlaySound("zap", item.WorldPosition, hullGuess: pt.item.CurrentHull); - + SoundPlayer.PlaySound("zap", item.WorldPosition, hullGuess: item.CurrentHull); Vector2 baseVel = Rand.Vector(300.0f); for (int i = 0; i < 10; i++) { - var particle = GameMain.ParticleManager.CreateParticle("spark", pt.item.WorldPosition, - baseVel + Rand.Vector(100.0f), 0.0f, pt.item.CurrentHull); - + var particle = GameMain.ParticleManager.CreateParticle("spark", item.WorldPosition, + baseVel + Rand.Vector(100.0f), 0.0f, item.CurrentHull); if (particle != null) particle.Size *= Rand.Range(0.5f, 1.0f); } #endif - - float currentIntensity = GameMain.GameSession?.EventManager != null ? + float currentIntensity = GameMain.GameSession?.EventManager != null ? GameMain.GameSession.EventManager.CurrentIntensity : 0.5f; - + //higher probability for fires if the current intensity is low - if (pt.FireProbability > 0.0f && - Rand.Range(0.0f, 1.0f) < MathHelper.Lerp(pt.FireProbability, pt.FireProbability * 0.1f, currentIntensity)) + if (FireProbability > 0.0f && + Rand.Range(0.0f, 1.0f) < MathHelper.Lerp(FireProbability, FireProbability * 0.1f, currentIntensity)) { - new FireSource(pt.item.WorldPosition); + new FireSource(item.WorldPosition); } } } - - updateCount = 0; } public override bool Pick(Character picker) @@ -243,7 +173,7 @@ namespace Barotrauma.Items.Components return picker != null; } - private void RefreshConnections() + protected void RefreshConnections() { var connections = item.Connections; foreach (Connection c in connections) @@ -317,102 +247,6 @@ namespace Barotrauma.Items.Components } } - //a recursive function that goes through all the junctions and adds up - //all the generated/consumed power of the constructions connected to the grid - private void CheckJunctions(float deltaTime, bool increaseUpdateCount = true, float clampPower = float.MaxValue, float clampLoad = float.MaxValue) - { - if (increaseUpdateCount) - { - updateCount = 1; - } - connectedList.Add(this); - - ApplyStatusEffects(ActionType.OnActive, deltaTime, null); - - //float maxPower = this is RelayComponent relayComponent ? relayComponent.MaxPower : float.PositiveInfinity; - RelayComponent thisRelayComponent = this as RelayComponent; - if (thisRelayComponent != null) - { - clampPower = Math.Min(Math.Min(clampPower, thisRelayComponent.MaxPower), powerLoad); - clampLoad = Math.Min(clampLoad, thisRelayComponent.MaxPower); - } - - foreach (Connection c in PowerConnections) - { - var recipients = c.Recipients; - foreach (Connection recipient in recipients) - { - if (recipient?.Item == null || !recipient.IsPower) { continue; } - - Item it = recipient.Item; - if (it.Condition <= 0.0f) { continue; } - - foreach (ItemComponent ic in it.Components) - { - if (!(ic is Powered powered) || !powered.IsActive) { continue; } - if (connectedList.Contains(powered)) { continue; } - - if (powered is PowerTransfer powerTransfer) - { - RelayComponent otherRelayComponent = powerTransfer as RelayComponent; - if ((thisRelayComponent == null) == (otherRelayComponent == null)) - { - if (!powerTransfer.CanTransfer) { continue; } - powerTransfer.CheckJunctions(deltaTime, increaseUpdateCount, clampPower, clampLoad); - } - else - { - if (!powerTransfer.CanTransfer) continue; - float maxPowerIn = (thisRelayComponent != null && c.IsOutput) ? 0.0f : clampPower; - float maxPowerOut = (thisRelayComponent != null && !c.IsOutput) ? 0.0f : clampLoad; - if (maxPowerIn > 0.0f || maxPowerOut > 0.0f) - { - powerTransfer.CheckJunctions(deltaTime, false, maxPowerIn, maxPowerOut); - } - } - - continue; - } - - float addLoad = 0.0f; - float addPower = 0.0f; - if (powered is PowerContainer powerContainer) - { - if (recipient.Name == "power_in") - { - addLoad = powerContainer.CurrPowerConsumption; - } - else - { - addPower = powerContainer.CurrPowerOutput; - } - } - else - { - connectedList.Add(powered); - //positive power consumption = the construction requires power -> increase load - if (powered.CurrPowerConsumption > 0.0f) - { - addLoad = powered.CurrPowerConsumption; - } - else if (powered.CurrPowerConsumption < 0.0f) - //negative power consumption = the construction is a - //generator/battery or another junction box - { - addPower -= powered.CurrPowerConsumption; - } - } - - if (addPower + fullPower > clampPower) { addPower -= (addPower + fullPower) - clampPower; }; - if (addPower > 0) { fullPower += addPower; } - - if (addLoad + fullLoad > clampLoad) { addLoad -= (addLoad + fullLoad) - clampLoad; }; - if (addLoad > 0) { fullLoad += addLoad; } - } - } - } - } - public void SetAllConnectionsDirty() { if (item.Connections == null) return; @@ -431,8 +265,9 @@ namespace Barotrauma.Items.Components public override void OnItemLoaded() { + base.OnItemLoaded(); var connections = Item.Connections; - powerConnections = connections == null ? new List() : connections.FindAll(c => c.IsPower); + PowerConnections = connections == null ? new List() : connections.FindAll(c => c.IsPower); if (connections == null) { IsActive = false; @@ -440,33 +275,45 @@ namespace Barotrauma.Items.Components } SetAllConnectionsDirty(); } - + + public override void ReceivePowerProbeSignal(Connection connection, Item source, float power) + { + //we've already received this signal + if (lastPowerProbeRecipients.Contains(this)) { return; } + lastPowerProbeRecipients.Add(this); + + if (power < 0.0f) + { + powerLoad -= power; + } + else + { + currPowerConsumption -= power; + } + powerOut?.SendPowerProbeSignal(source, power); + } + public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power, float signalStrength = 1.0f) { - if (connection.IsPower) return; - - base.ReceiveSignal(stepsTaken, signal, connection, source, sender, power); - - if (!connectedRecipients.ContainsKey(connection)) return; + if (item.Condition <= 0.0f || connection.IsPower) { return; } + if (!connectedRecipients.ContainsKey(connection)) { return; } if (connection.Name.Length > 5 && connection.Name.Substring(0, 6) == "signal") { foreach (Connection recipient in connectedRecipients[connection]) { - if (recipient.Item == item || recipient.Item == source) continue; + if (recipient.Item == item || recipient.Item == source) { continue; } foreach (ItemComponent ic in recipient.Item.Components) { //powertransfer components don't need to receive the signal in the pass-through signal connections //because we relay it straight to the connected items without going through the whole chain of junction boxes - if (ic is PowerTransfer && connection.Name.Contains("signal")) continue; + if (ic is PowerTransfer && connection.Name.Contains("signal")) { continue; } ic.ReceiveSignal(stepsTaken, signal, recipient, source, sender, 0.0f, signalStrength); } - bool broken = recipient.Item.Condition <= 0.0f; foreach (StatusEffect effect in recipient.Effects) { - if (broken && effect.type != ActionType.OnBroken) continue; recipient.Item.ApplyStatusEffect(effect, ActionType.OnUse, 1.0f, null, null, false, false); } } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Power/Powered.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Power/Powered.cs index fb6627510..37c452648 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Power/Powered.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Power/Powered.cs @@ -1,6 +1,7 @@ using System; using System.Xml.Linq; using Microsoft.Xna.Framework; +using System.Collections.Generic; #if CLIENT using Barotrauma.Sounds; #endif @@ -9,25 +10,47 @@ namespace Barotrauma.Items.Components { partial class Powered : ItemComponent { - //the amount of power CURRENTLY consumed by the item - //negative values mean that the item is providing power to connected items + private static float updateTimer; + protected static float UpdateInterval = 0.2f; + + /// + /// List of all powered ItemComponents + /// + private static readonly List poweredList = new List(); + + /// + /// Items that have already received the "probe signal" that's used to distribute power and load across the grid + /// + protected static HashSet lastPowerProbeRecipients = new HashSet(); + + /// + /// The amount of power currently consumed by the item. Negative values mean that the item is providing power to connected items + /// protected float currPowerConsumption; - //current voltage of the item (load / power) - protected float voltage; + /// + /// Current voltage of the item (load / power) + /// + private float voltage; - //the minimum voltage required for the item to work - protected float minVoltage; + /// + /// The minimum voltage required for the item to work + /// + private float minVoltage; - //the maximum amount of power the item can draw from connected items + /// + /// The maximum amount of power the item can draw from connected items + /// protected float powerConsumption; + protected Connection powerIn, powerOut; + [Editable, Serialize(0.5f, true, description: "The minimum voltage required for the device to function. " + "The voltage is calculated as power / powerconsumption, meaning that a device " + "with a power consumption of 1000 kW would need at least 500 kW of power to work if the minimum voltage is set to 0.5.")] public float MinVoltage { - get { return minVoltage; } + get { return powerConsumption <= 0.0f ? 0.0f : minVoltage; } set { minVoltage = value; } } @@ -76,34 +99,32 @@ namespace Barotrauma.Items.Components public Powered(Item item, XElement element) : base(item, element) { + poweredList.Add(this); InitProjectSpecific(element); } partial void InitProjectSpecific(XElement element); - public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1.0f) - { - if (currPowerConsumption == 0.0f) voltage = 0.0f; - if (connection.IsPower) voltage = Math.Max(0.0f, power); - } - protected void UpdateOnActiveEffects(float deltaTime) { - if (currPowerConsumption == 0.0f) + if (currPowerConsumption <= 0.0f) { //if the item consumes no power, ignore the voltage requirement and //apply OnActive statuseffects as long as this component is active - if (powerConsumption == 0.0f) + if (powerConsumption <= 0.0f) { ApplyStatusEffects(ActionType.OnActive, deltaTime, null); } return; } -#if CLIENT if (voltage > minVoltage) { ApplyStatusEffects(ActionType.OnActive, deltaTime, null); + } +#if CLIENT + if (voltage > minVoltage) + { if (!powerOnSoundPlayed && powerOnSound != null) { SoundPlayer.PlaySound(powerOnSound.Sound, item.WorldPosition, powerOnSound.Volume, powerOnSound.Range, item.CurrentHull); @@ -114,21 +135,160 @@ namespace Barotrauma.Items.Components { powerOnSoundPlayed = false; } -#else - if (voltage > minVoltage) - { - ApplyStatusEffects(ActionType.OnActive, deltaTime, null); - } #endif } public override void Update(float deltaTime, Camera cam) { UpdateOnActiveEffects(deltaTime); - - voltage = 0.0f; } + public override void OnItemLoaded() + { + if (item.Connections == null) { return; } + foreach (Connection c in item.Connections) + { + if (!c.IsPower) { continue; } + if (this is PowerTransfer pt) + { + if (c.Name == "power_in") + { + powerIn = c; + } + else if (c.Name == "power_out") + { + powerOut = c; + } + else if (c.Name == "power") + { + powerIn = powerOut = c; + } + } + else + { + if (c.IsOutput) + { + if (c.Name == "power_in") + { +#if DEBUG + DebugConsole.ThrowError($"Item \"{item.Name}\" has a power output connection called power_in. If the item is supposed to receive power through the connection, change it to an input connection."); +#else + DebugConsole.NewMessage($"Item \"{item.Name}\" has a power output connection called power_in. If the item is supposed to receive power through the connection, change it to an input connection.", Color.Orange); +#endif + } + powerOut = c; + } + else + { + if (c.Name == "power_out") + { +#if DEBUG + DebugConsole.ThrowError($"Item \"{item.Name}\" has a power input connection called power_out. If the item is supposed to output power through the connection, change it to an output connection."); +#else + DebugConsole.NewMessage($"Item \"{item.Name}\" has a power input connection called power_out. If the item is supposed to output power through the connection, change it to an output connection.", Color.Orange); +#endif + } + powerIn = c; + } + } + } + } + + public virtual void ReceivePowerProbeSignal(Connection connection, Item source, float power) { } + public static void UpdatePower(float deltaTime) + { + if (updateTimer > 0.0f) + { + updateTimer -= deltaTime; + return; + } + updateTimer = UpdateInterval; + + //reset power first + foreach (Powered powered in poweredList) + { + if (powered is PowerTransfer pt) + { + powered.CurrPowerConsumption = 0.0f; + pt.PowerLoad = 0.0f; + if (pt is RelayComponent relay) + { + relay.DisplayLoad = 0.0f; + } + } + //only reset voltage if the item has a power connector + //(other items, such as handheld devices, get power through other means and shouldn't be updated here) + if (powered.powerIn != null || powered.powerOut != null) { powered.voltage = 0.0f; } + } + + //go through all the devices that are consuming/providing power + //and send out a "probe signal" which the PowerTransfer components use to add up the grid power/load + foreach (Powered powered in poweredList) + { + if (powered is PowerTransfer) { continue; } + if (powered.currPowerConsumption > 0.0f) + { + //consuming power + lastPowerProbeRecipients.Clear(); + powered.powerIn?.SendPowerProbeSignal(powered.item, -powered.currPowerConsumption); + } + } + foreach (Powered powered in poweredList) + { + if (powered is PowerTransfer) { continue; } + else if (powered.currPowerConsumption < 0.0f) + { + //providing power + lastPowerProbeRecipients.Clear(); + powered.powerOut?.SendPowerProbeSignal(powered.item, -powered.currPowerConsumption); + } + if (powered is PowerContainer pc) + { + if (pc.CurrPowerOutput <= 0.0f) { continue; } + //providing power + lastPowerProbeRecipients.Clear(); + powered.powerOut?.SendPowerProbeSignal(powered.item, pc.CurrPowerOutput); + } + } + //go through powered items and calculate their current voltage + foreach (Powered powered in poweredList) + { + if (powered is PowerTransfer pt1 || (pt1 = powered.Item.GetComponent()) != null) + { + powered.voltage = -pt1.CurrPowerConsumption / Math.Max(pt1.PowerLoad, 1.0f); + continue; + } + if (powered.powerConsumption <= 0.0f && !(powered is PowerContainer)) + { + powered.voltage = 1.0f; + continue; + } + if (powered.powerIn == null) { continue; } + + foreach (Connection powerSource in powered.powerIn.Recipients) + { + if (!powerSource.IsPower || !powerSource.IsOutput) { continue; } + var pt = powerSource.Item.GetComponent(); + if (pt != null) + { + float voltage = -pt.CurrPowerConsumption / Math.Max(pt.PowerLoad, 1.0f); + powered.voltage = Math.Max(powered.voltage, voltage); + continue; + } + var pc = powerSource.Item.GetComponent(); + if (pc != null) + { + float voltage = -pc.CurrPowerOutput / Math.Max(powered.CurrPowerConsumption, 1.0f); + powered.voltage += voltage; + } + } + } + } + + protected override void RemoveComponentSpecific() + { + poweredList.Remove(this); + } } } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Projectile.cs index ec2827d76..c9fd76605 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Projectile.cs @@ -416,9 +416,8 @@ namespace Barotrauma.Items.Components if (IgnoredBodies.Contains(target.Body)) { return false; } - if (target.UserData is Item) { return false; } - - if (target.CollisionCategories == Physics.CollisionCharacter && !(target.Body.UserData is Limb)) + //ignore character colliders (the projectile only hits limbs) + if (target.CollisionCategories == Physics.CollisionCharacter && target.Body.UserData is Character) { return false; } @@ -445,17 +444,52 @@ namespace Barotrauma.Items.Components if (attack != null) { attackResult = attack.DoDamageToLimb(User, limb, item.WorldPosition, 1.0f); } if (limb.character != null) { character = limb.character; } } - else if (target.Body.UserData is Structure structure) + else if (target.Body.UserData is Item targetItem) { - if (attack != null) { attackResult = attack.DoDamage(User, structure, item.WorldPosition, 1.0f); } + if (attack != null && targetItem.Prefab.DamagedByProjectiles) + { + attackResult = attack.DoDamage(User, targetItem, item.WorldPosition, 1.0f); + } + } + else if (target.Body.UserData is IDamageable damageable) + { + if (attack != null) { attackResult = attack.DoDamage(User, damageable, item.WorldPosition, 1.0f); } } if (character != null) { character.LastDamageSource = item; } if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { - ApplyStatusEffects(ActionType.OnUse, 1.0f, character, target.Body.UserData as Limb, user: user); - ApplyStatusEffects(ActionType.OnImpact, 1.0f, character, target.Body.UserData as Limb, user: user); + if (target.Body.UserData is Limb targetLimb) + { + ApplyStatusEffects(ActionType.OnUse, 1.0f, character, targetLimb, user: user); + ApplyStatusEffects(ActionType.OnImpact, 1.0f, character, targetLimb, user: user); + var attack = targetLimb.attack; + if (attack != null) + { + // Apply the status effects defined in the limb's attack that was hit + foreach (var effect in attack.StatusEffects) + { + if (effect.type == ActionType.OnImpact) + { + //effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb.character, targetLimb.WorldPosition); + + if (effect.HasTargetType(StatusEffect.TargetType.This)) + { + effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb.character, targetLimb.WorldPosition); + } + if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || + effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) + { + var targets = new List(); + effect.GetNearbyTargets(targetLimb.WorldPosition, targets); + effect.Apply(ActionType.OnActive, 1.0f, targetLimb.character, targets); + } + + } + } + } + } #if SERVER if (GameMain.NetworkMember.IsServer) { diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/Connection.cs index cb9a09a9d..e029a9e9b 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/Connection.cs @@ -92,9 +92,7 @@ namespace Barotrauma.Items.Components foreach (XElement connectionElement in subElement.Elements()) { - if (connectionElement.Name.ToString() != element.Name.ToString()) { continue; } - - string prefabConnectionName = element.GetAttributeString("name", IsOutput ? "output" : "input"); + string prefabConnectionName = element.GetAttributeString("name", null); if (prefabConnectionName == Name) { displayNameTag = connectionElement.GetAttributeString("displayname", ""); @@ -245,31 +243,38 @@ namespace Barotrauma.Items.Components { for (int i = 0; i < MaxLinked; i++) { - if (wires[i] == null) continue; + if (wires[i] == null) { continue; } Connection recipient = wires[i].OtherConnection(this); - if (recipient == null) continue; - if (recipient.item == this.item || recipient.item == source) continue; + if (recipient == null) { continue; } + if (recipient.item == this.item || recipient.item == source) { continue; } - if (source != null && !source.LastSentSignalRecipients.Contains(recipient.item)) - { - source.LastSentSignalRecipients.Add(recipient.item); - } + source?.LastSentSignalRecipients.Add(recipient.item); foreach (ItemComponent ic in recipient.item.Components) { ic.ReceiveSignal(stepsTaken, signal, recipient, source, sender, power, signalStrength); } - bool broken = recipient.Item.Condition <= 0.0f; foreach (StatusEffect effect in recipient.Effects) { - if (broken && effect.type != ActionType.OnBroken) continue; recipient.Item.ApplyStatusEffect(effect, ActionType.OnUse, (float)Timing.Step, null, null, false, false); } } } + public void SendPowerProbeSignal(Item source, float power) + { + for (int i = 0; i < MaxLinked; i++) + { + if (wires[i] == null) { continue; } + + Connection recipient = wires[i].OtherConnection(this); + if (recipient == null) { continue; } + + recipient.item.GetComponent()?.ReceivePowerProbeSignal(recipient, source, power); + } + } public void ClearConnections() { for (int i = 0; i < MaxLinked; i++) diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/FunctionComponent.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/FunctionComponent.cs new file mode 100644 index 000000000..77bd2edb1 --- /dev/null +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/FunctionComponent.cs @@ -0,0 +1,57 @@ +using System; +using System.Globalization; +using System.Xml.Linq; + +namespace Barotrauma.Items.Components +{ + class FunctionComponent : ItemComponent + { + public enum FunctionType + { + Round, + Ceil, + Floor, + Factorial + } + + [Serialize(FunctionType.Round, false, description: "Which kind of function to run the input through.")] + public FunctionType Function + { + get; set; + } + + public FunctionComponent(Item item, XElement element) + : base(item, element) + { + IsActive = true; + } + + public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1) + { + float.TryParse(signal, out float value); + switch (Function) + { + case FunctionType.Round: + item.SendSignal(0, Math.Round(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + break; + case FunctionType.Ceil: + item.SendSignal(0, Math.Ceiling(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + break; + case FunctionType.Floor: + item.SendSignal(0, Math.Floor(value).ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + break; + case FunctionType.Factorial: + int intVal = (int)Math.Min(value, 20); + ulong factorial = 1; + for (int i = intVal; i > 0; i--) + { + factorial *= (ulong)i; + } + item.SendSignal(0, factorial.ToString(), "signal_out", null); + break; + default: + throw new NotImplementedException($"Function {Function} has not been implemented."); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/LightComponent.cs index 3aa438c7e..0f7a815d0 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/LightComponent.cs @@ -216,19 +216,12 @@ namespace Barotrauma.Items.Components #endif } - if (powerConsumption == 0.0f) - { - voltage = 1.0f; - } - else - { - currPowerConsumption = powerConsumption; - } + currPowerConsumption = powerConsumption; - if (Rand.Range(0.0f, 1.0f) < 0.05f && voltage < Rand.Range(0.0f, minVoltage)) + if (Rand.Range(0.0f, 1.0f) < 0.05f && Voltage < Rand.Range(0.0f, MinVoltage)) { #if CLIENT - if (voltage > 0.1f) + if (Voltage > 0.1f) { SoundPlayer.PlaySound("zap", item.WorldPosition, hullGuess: item.CurrentHull); } @@ -237,7 +230,7 @@ namespace Barotrauma.Items.Components } else { - lightBrightness = MathHelper.Lerp(lightBrightness, Math.Min(voltage, 1.0f), 0.1f); + lightBrightness = MathHelper.Lerp(lightBrightness, Math.Min(Voltage, 1.0f), 0.1f); } if (blinkFrequency > 0.0f) @@ -262,8 +255,6 @@ namespace Barotrauma.Items.Components { UpdateAITarget(item.AiTarget); } - - voltage -= deltaTime; } #if CLIENT diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/ModuloComponent.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/ModuloComponent.cs new file mode 100644 index 000000000..8893d1351 --- /dev/null +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/ModuloComponent.cs @@ -0,0 +1,41 @@ +using System.Globalization; +using System.Xml.Linq; + +namespace Barotrauma.Items.Components +{ + class ModuloComponent : ItemComponent + { + private float modulus; + [InGameEditable, Serialize(1.0f, false, description: "The modulus of the operation. Must be non-zero.")] + public float Modulus + { + get { return modulus; } + set + { + modulus = MathUtils.NearlyEqual(value, 0.0f) ? 1.0f : value; + } + } + + public ModuloComponent(Item item, XElement element) : base(item, element) + { + IsActive = true; + } + + public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1) + { + switch (connection.Name) + { + case "set_modulus": + case "modulus": + float.TryParse(signal, out float newModulus); + Modulus = newModulus; + break; + case "signal_in": + float.TryParse(signal, out float value); + item.SendSignal(0, (value % modulus).ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + break; + } + + } + } +} diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/MotionSensor.cs index 175bd4399..a672b8e63 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/MotionSensor.cs @@ -25,6 +25,14 @@ namespace Barotrauma.Items.Components set; } + [Editable, Serialize(false, true, description: "Should the sensor ignore the bodies of dead characters?")] + public bool IgnoreDead + { + get; + set; + } + + [InGameEditable, Serialize(0.0f, true, description: "Horizontal detection range.")] public float RangeX { @@ -109,6 +117,7 @@ namespace Barotrauma.Items.Components foreach (Character c in Character.CharacterList) { + if (IgnoreDead && c.IsDead) { continue; } if (OnlyHumans && !c.IsHuman) { continue; } //do a rough check based on the position of the character's collider first @@ -138,5 +147,15 @@ namespace Barotrauma.Items.Components { detectOffset.Y = -detectOffset.Y; } + public override XElement Save(XElement parentElement) + { + Vector2 prevDetectOffset = detectOffset; + //undo flipping before saving + if (item.FlippedX) { detectOffset.X = -detectOffset.X; } + if (item.FlippedY) { detectOffset.Y = -detectOffset.Y; } + XElement element = base.Save(parentElement); + detectOffset = prevDetectOffset; + return element; + } } } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/RelayComponent.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/RelayComponent.cs index 7fe73ed1b..26c197604 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/RelayComponent.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/RelayComponent.cs @@ -1,4 +1,5 @@ using Barotrauma.Networking; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Xml.Linq; @@ -8,9 +9,11 @@ namespace Barotrauma.Items.Components class RelayComponent : PowerTransfer, IServerSerializable { private float maxPower; - + private bool isOn; + private float throttlePowerOutput; + private static readonly Dictionary connectionPairs = new Dictionary { { "power_in", "power_out"}, @@ -21,6 +24,7 @@ namespace Barotrauma.Items.Components { "signal_in4", "signal_out4" }, { "signal_in5", "signal_out5" } }; + public float DisplayLoad { get; set; } [Editable, Serialize(1000.0f, true, description: "The maximum amount of power that can pass through the item.")] public float MaxPower @@ -31,7 +35,7 @@ namespace Barotrauma.Items.Components maxPower = Math.Max(0.0f, value); } } - + [Editable, Serialize(false, true, description: "Can the relay currently pass power and signals through it.")] public bool IsOn { @@ -49,18 +53,46 @@ namespace Barotrauma.Items.Components } } } - + public RelayComponent(Item item, XElement element) - : base (item, element) + : base(item, element) { IsActive = true; - } - + throttlePowerOutput = MaxPower; + } public override void Update(float deltaTime, Camera cam) { - base.Update(deltaTime, cam); + RefreshConnections(); item.SendSignal(0, IsOn ? "1" : "0", "state_out", null); + + if (!CanTransfer) { Voltage = 0.0f; return; } + + if (isBroken) + { + SetAllConnectionsDirty(); + isBroken = false; + } + + ApplyStatusEffects(ActionType.OnActive, deltaTime, null); + + if (powerOut != null) + { + bool overloaded = false; + foreach (Connection recipient in powerOut.Recipients) + { + var pt = recipient.Item.GetComponent(); + if (pt != null) + { + float overload = -pt.CurrPowerConsumption - pt.PowerLoad; + throttlePowerOutput += overload * deltaTime * 0.5f; + overloaded = overload > 1.0f; + } + } + throttlePowerOutput = overloaded ? + MathHelper.Clamp(throttlePowerOutput, 0.0f, MaxPower): + Math.Max(throttlePowerOutput - MaxPower * 0.1f * deltaTime, 0.0f); + } if (Math.Min(-currPowerConsumption, PowerLoad) > maxPower && CanBeOverloaded) { @@ -68,9 +100,56 @@ namespace Barotrauma.Items.Components } } + public override void ReceivePowerProbeSignal(Connection connection, Item source, float power) + { + if (!IsOn) { return; } + + //we've already received this signal + if (lastPowerProbeRecipients.Contains(this)) { return; } + lastPowerProbeRecipients.Add(this); + + if (power < 0.0f) + { + if (!connection.IsOutput || powerIn == null) { return; } + + //power being drawn from the power_out connection + DisplayLoad -= Math.Min(power, 0.0f); + powerLoad -= Math.Min(power + throttlePowerOutput, 0.0f); + + //pass the load to items connected to the input + powerIn.SendPowerProbeSignal(source, Math.Max(power, -MaxPower)); + } + else + { + if (connection.IsOutput || powerOut == null) { return; } + //power being supplied to the power_in connection + if (currPowerConsumption - power < -MaxPower) + { + power += MaxPower + (currPowerConsumption - power); + } + + currPowerConsumption -= power; + + foreach (Connection recipient in powerOut.Recipients) + { + if (!recipient.IsPower) { continue; } + var powered = recipient.Item.GetComponent(); + if (powered == null) { continue; } + + float load = powered.CurrPowerConsumption; + var powerTransfer = powered as PowerTransfer; + if (powerTransfer != null) { load = powerTransfer.PowerLoad; } + + float powerOut = power * (load / Math.Max(powerLoad + throttlePowerOutput, 0.01f)); + powered.ReceivePowerProbeSignal(recipient, source, Math.Min(powerOut, power)); + } + } + + } + public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) { - if (connection.IsPower || item.Condition <= 0.0f) { return; } + if (item.Condition <= 0.0f || connection.IsPower) { return; } if (connectionPairs.TryGetValue(connection.Name, out string outConnection)) { diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/TrigonometricFunctionComponent.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/TrigonometricFunctionComponent.cs new file mode 100644 index 000000000..05bca8b5e --- /dev/null +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/TrigonometricFunctionComponent.cs @@ -0,0 +1,108 @@ +using Microsoft.Xna.Framework; +using System; +using System.Globalization; +using System.Xml.Linq; + +namespace Barotrauma.Items.Components +{ + class TrigonometricFunctionComponent : ItemComponent + { + public enum FunctionType + { + Sin, + Cos, + Tan, + Asin, + Acos, + Atan, + } + + protected float[] receivedSignal = new float[2]; + + [Serialize(FunctionType.Sin, false, description: "Which kind of function to run the input through.")] + public FunctionType Function + { + get; set; + } + + + [InGameEditable, Serialize(false, true, description: "If set to true, the trigonometric function uses radians instead of degrees.")] + public bool UseRadians + { + get; set; + } + + + public TrigonometricFunctionComponent(Item item, XElement element) + : base(item, element) + { + IsActive = true; + } + + public override void Update(float deltaTime, Camera cam) + { + //reset received signals + receivedSignal[0] = float.NaN; + receivedSignal[1] = float.NaN; + } + + + public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1) + { + float.TryParse(signal, out float value); + switch (Function) + { + case FunctionType.Sin: + if (!UseRadians) { value = MathHelper.ToRadians(value); } + item.SendSignal(0, ((float)Math.Sin(value)).ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + break; + case FunctionType.Cos: + if (!UseRadians) { value = MathHelper.ToRadians(value); } + item.SendSignal(0, ((float)Math.Cos(value)).ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + break; + case FunctionType.Tan: + if (!UseRadians) { value = MathHelper.ToRadians(value); } + item.SendSignal(0, ((float)Math.Tan(value)).ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + break; + case FunctionType.Asin: + { + float angle = (float)Math.Asin(value); + if (!UseRadians) { angle = MathHelper.ToDegrees(angle); } + item.SendSignal(0, angle.ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + } + break; + case FunctionType.Acos: + { + float angle = (float)Math.Acos(value); + if (!UseRadians) { angle = MathHelper.ToDegrees(angle); } + item.SendSignal(0, angle.ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + } + break; + case FunctionType.Atan: + if (connection.Name == "signal_in_x") + { + float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[0]); + } + else if (connection.Name == "signal_in_y") + { + float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out receivedSignal[1]); + if (!float.IsNaN(receivedSignal[0]) && !float.IsNaN(receivedSignal[1])) + { + float angle = (float)Math.Atan2(receivedSignal[1], receivedSignal[0]); + if (!UseRadians) { angle = MathHelper.ToDegrees(angle); } + item.SendSignal(0, angle.ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + } + } + else + { + float angle = (float)Math.Atan(value); + if (!UseRadians) { angle = MathHelper.ToDegrees(angle); } + item.SendSignal(0, angle.ToString("G", CultureInfo.InvariantCulture), "signal_out", null); + } + break; + default: + throw new NotImplementedException($"Function {Function} has not been implemented."); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/WifiComponent.cs index b14ec6b00..f78b70c90 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/WifiComponent.cs @@ -22,7 +22,7 @@ namespace Barotrauma.Items.Components [Serialize(Character.TeamType.None, false, description: "WiFi components can only communicate with components that have the same Team ID.")] public Character.TeamType TeamID { get; set; } - [Serialize(20000.0f, false, description: "How close the recipient has to be to receive a signal from this WiFi component.")] + [Editable, Serialize(20000.0f, false, description: "How close the recipient has to be to receive a signal from this WiFi component.")] public float Range { get { return range; } @@ -174,8 +174,6 @@ namespace Barotrauma.Items.Components protected override void RemoveComponentSpecific() { - base.RemoveComponentSpecific(); - list.Remove(this); } } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/Wire.cs index a2e0ae132..0ff58668f 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Signal/Wire.cs @@ -83,22 +83,16 @@ namespace Barotrauma.Items.Components public Wire(Item item, XElement element) : base(item, element) { -#if CLIENT - if (wireSprite == null) - { - wireSprite = new Sprite("Content/Items/wireHorizontal.png", new Vector2(0.5f, 0.5f)) - { - Depth = 0.85f - }; - } -#endif - nodes = new List(); sections = new List(); connections = new Connection[2]; IsActive = false; + + InitProjSpecific(element); } + partial void InitProjSpecific(XElement element); + public Connection OtherConnection(Connection connection) { if (connection == connections[0]) { return connections[1]; } @@ -728,6 +722,11 @@ namespace Barotrauma.Items.Components { ClearConnections(); base.RemoveComponentSpecific(); +#if CLIENT + overrideSprite?.Remove(); + overrideSprite = null; + wireSprite = null; +#endif } public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Turret.cs index 8cafa873f..fe184c492 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Turret.cs @@ -192,6 +192,7 @@ namespace Barotrauma.Items.Components public override void OnItemLoaded() { + base.OnItemLoaded(); var lightComponents = item.GetComponents(); if (lightComponents != null && lightComponents.Count() > 0) { @@ -325,20 +326,24 @@ namespace Barotrauma.Items.Components failedLaunchAttempts = 0; var batteries = item.GetConnectedComponents(); - float availablePower = 0.0f; - foreach (PowerContainer battery in batteries) + float neededPower = powerConsumption; + + while (neededPower > 0.0001f && batteries.Count > 0) { - float batteryPower = Math.Min(battery.Charge * 3600.0f, battery.MaxOutPut); - float takePower = Math.Min(powerConsumption - availablePower, batteryPower); - - battery.Charge -= takePower / 3600.0f; - -#if SERVER - if (GameMain.Server != null) + batteries.RemoveAll(b => b.Charge <= 0.0001f || b.MaxOutPut <= 0.0001f); + float takePower = neededPower / batteries.Count; + takePower = Math.Min(takePower, batteries.Min(b => Math.Min(b.Charge * 3600.0f, b.MaxOutPut))); + foreach (PowerContainer battery in batteries) { - battery.Item.CreateServerEvent(battery); - } + neededPower -= takePower; + battery.Charge -= takePower / 3600.0f; +#if SERVER + if (GameMain.Server != null) + { + battery.Item.CreateServerEvent(battery); + } #endif + } } Launch(projectiles[0].Item, character); @@ -477,12 +482,12 @@ namespace Barotrauma.Items.Components //enough shells and power Character closestEnemy = null; - float closestDist = 10000.0f * 10000.0f; + float closestDist = 3000 * 3000; foreach (Character enemy in Character.CharacterList) { - //ignore humans and characters that are inside the sub - if (enemy.IsDead|| enemy.AnimController.CurrentHull != null || !enemy.Enabled) { continue; } - if (enemy.SpeciesName == character.SpeciesName && enemy.TeamID == character.TeamID) { continue; } + // Ignore friendly and those that are inside the sub + if (enemy.IsDead || enemy.AnimController.CurrentHull != null || !enemy.Enabled) { continue; } + if (HumanAIController.IsFriendly(character, enemy)) { continue; } float dist = Vector2.DistanceSquared(enemy.WorldPosition, item.WorldPosition); if (dist > closestDist) { continue; } @@ -510,8 +515,21 @@ namespace Barotrauma.Items.Components if (Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) > 0.15f) { return false; } - var pickedBody = Submarine.PickBody(ConvertUnits.ToSimUnits(item.WorldPosition), closestEnemy.SimPosition, null); - if (pickedBody != null && !(pickedBody.UserData is Limb)) { return false; } + var pickedBody = Submarine.PickBody(ConvertUnits.ToSimUnits(item.WorldPosition), closestEnemy.SimPosition); + if (pickedBody == null) { return false; } + Character target = null; + if (pickedBody.UserData is Character c) + { + target = c; + } + else if (pickedBody.UserData is Limb limb) + { + target = limb.character; + } + if (target == null || HumanAIController.IsFriendly(character, target)) + { + return false; + } if (objective.Option.ToLowerInvariant() == "fireatwill") { @@ -554,8 +572,8 @@ namespace Barotrauma.Items.Components { base.RemoveComponentSpecific(); - if (barrelSprite != null) barrelSprite.Remove(); - if (railSprite != null) railSprite.Remove(); + barrelSprite?.Remove(); barrelSprite = null; + railSprite?.Remove(); railSprite = null; #if CLIENT moveSoundChannel?.Dispose(); moveSoundChannel = null; diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Wearable.cs index 469a42f5c..2c9988a89 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Wearable.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Xml.Linq; using Barotrauma.Items.Components; using Barotrauma.Extensions; +using Barotrauma.Networking; namespace Barotrauma { @@ -23,6 +24,7 @@ namespace Barotrauma class WearableSprite { + public string UnassignedSpritePath { get; private set; } public string SpritePath { get; private set; } public XElement SourceElement { get; private set; } @@ -83,7 +85,7 @@ namespace Barotrauma if (value == _gender) { return; } _gender = value; IsInitialized = false; - SpritePath = ParseSpritePath(SourceElement.GetAttributeString("texture", string.Empty)); + UnassignedSpritePath = ParseSpritePath(SourceElement.GetAttributeString("texture", string.Empty)); Init(_gender); } } @@ -92,7 +94,7 @@ namespace Barotrauma { Type = type; SourceElement = subElement; - SpritePath = subElement.GetAttributeString("texture", string.Empty); + UnassignedSpritePath = subElement.GetAttributeString("texture", string.Empty); Init(); switch (type) { @@ -122,42 +124,24 @@ namespace Barotrauma Type = WearableType.Item; WearableComponent = wearable; Variant = Math.Max(variant, 0); - SpritePath = ParseSpritePath(subElement.GetAttributeString("texture", string.Empty)); + UnassignedSpritePath = ParseSpritePath(subElement.GetAttributeString("texture", string.Empty)); SourceElement = subElement; } private string ParseSpritePath(string texturePath) => texturePath.Contains("/") ? texturePath : $"{Path.GetDirectoryName(WearableComponent.Item.Prefab.ConfigFile)}/{texturePath}"; - public void RefreshPath() - { - if (Variant > 0) - { - // Restore the tag so that we can parse it again. - ReplaceNumbersWith("[VARIANT]"); - } - ParsePath(true); - } - - private void ReplaceNumbersWith(string replacement) - { - var fileName = Path.GetFileName(SpritePath); - var path = Path.GetDirectoryName(SpritePath); - fileName = fileName.Replace(replacement, c => char.IsNumber(c)); - SpritePath = Path.Combine(path, fileName); - } - - private void ParsePath(bool parseSpritePath) + public void ParsePath(bool parseSpritePath) { + string tempPath = UnassignedSpritePath; if (_gender != Gender.None) { - SpritePath = SpritePath.Replace("[GENDER]", (_gender == Gender.Female) ? "female" : "male"); + tempPath = tempPath.Replace("[GENDER]", (_gender == Gender.Female) ? "female" : "male"); } - SpritePath = SpritePath.Replace("[VARIANT]", Variant.ToString()); + SpritePath = tempPath.Replace("[VARIANT]", Variant.ToString()); if (!File.Exists(SpritePath)) { // If the variant does not exist, parse the path so that it uses first variant. - Variant = 1; - ReplaceNumbersWith(Variant.ToString()); + SpritePath = tempPath.Replace("[VARIANT]", "1"); } if (parseSpritePath) { @@ -169,13 +153,13 @@ namespace Barotrauma public void Init(Gender gender = Gender.None) { if (IsInitialized) { return; } - _gender = SpritePath.Contains("[GENDER]") ? gender : Gender.None; + _gender = UnassignedSpritePath.Contains("[GENDER]") ? gender : Gender.None; ParsePath(false); if (Sprite != null) { Sprite.Remove(); } - Sprite = new Sprite(SourceElement, file: SpritePath); + Sprite = new Sprite(SourceElement, file: SpritePath, preMultiplyAlpha: true); Limb = (LimbType)Enum.Parse(typeof(LimbType), SourceElement.GetAttributeString("limb", "Head"), true); HideLimb = SourceElement.GetAttributeBool("hidelimb", false); HideOtherWearables = SourceElement.GetAttributeBool("hideotherwearables", false); @@ -197,25 +181,60 @@ namespace Barotrauma namespace Barotrauma.Items.Components { - class Wearable : Pickable + class Wearable : Pickable, IServerSerializable { - private WearableSprite[] wearableSprites; - private LimbType[] limbType; - private Limb[] limb; + private readonly XElement[] wearableElements; + private readonly WearableSprite[] wearableSprites; + private readonly LimbType[] limbType; + private readonly Limb[] limb; - private List damageModifiers; + private readonly List damageModifiers; - public List DamageModifiers + public IEnumerable DamageModifiers { get { return damageModifiers; } } - private bool autoEquipWhenFull; - public bool AutoEquipWhenFull + public bool AutoEquipWhenFull { get; private set; } + + public readonly int Variants; + + private int variant; + public int Variant { - get { return autoEquipWhenFull; } - } - + get { return variant; } + set + { +#if SERVER + variant = value; + item.CreateServerEvent(this); +#elif CLIENT + if (variant == value) { return; } + + Character character = picker; + if (character != null) + { + Unequip(character); + } + + for (int i = 0; i < wearableSprites.Length; i++) + { + var subElement = wearableElements[i]; + + wearableSprites[i]?.Sprite?.Remove(); + wearableSprites[i] = new WearableSprite(subElement, this, value); + } + + if (character != null) + { + Equip(character); + } + + variant = value; +#endif + } + } + public Wearable(Item item, XElement element) : base(item, element) { this.item = item; @@ -223,12 +242,13 @@ namespace Barotrauma.Items.Components damageModifiers = new List(); int spriteCount = element.Elements().Count(x => x.Name.ToString() == "sprite"); - int variants = element.GetAttributeInt("variants", 0); - int variant = variants > 0 ? Rand.Range(1, variants + 1, Rand.RandSync.Server) : 1; + Variants = element.GetAttributeInt("variants", 0); + variant = Rand.Range(1, Variants + 1, Rand.RandSync.Server); wearableSprites = new WearableSprite[spriteCount]; + wearableElements = new XElement[spriteCount]; limbType = new LimbType[spriteCount]; limb = new Limb[spriteCount]; - autoEquipWhenFull = element.GetAttributeBool("autoequipwhenfull", true); + AutoEquipWhenFull = element.GetAttributeBool("autoequipwhenfull", true); int i = 0; foreach (XElement subElement in element.Elements()) { @@ -245,6 +265,7 @@ namespace Barotrauma.Items.Components subElement.GetAttributeString("limb", "Head"), true); wearableSprites[i] = new WearableSprite(subElement, this, variant); + wearableElements[i] = subElement; foreach (XElement lightElement in subElement.Elements()) { @@ -380,5 +401,39 @@ namespace Barotrauma.Items.Components } } + public override XElement Save(XElement parentElement) + { + XElement componentElement = base.Save(parentElement); + componentElement.Add(new XAttribute("variant", variant)); + return componentElement; + } + + private int loadedVariant = -1; + public override void Load(XElement componentElement, bool usePrefabValues) + { + base.Load(componentElement, usePrefabValues); + loadedVariant = componentElement.GetAttributeInt("variant", -1); + } + public override void OnItemLoaded() + { + base.OnItemLoaded(); + //do this here to prevent creating a network event before the item has been fully initialized + if (loadedVariant > 0 && loadedVariant < Variants + 1) + { + Variant = loadedVariant; + } + } + public override void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + { + msg.Write((byte)Variant); + base.ServerWrite(msg, c, extraData); + } + + public override void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + { + Variant = (int)msg.ReadByte(); + base.ClientRead(type, msg, sendingTime); + } + } } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Inventory.cs b/Barotrauma/BarotraumaShared/Source/Items/Inventory.cs index 692a9e94c..b68cb412b 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Inventory.cs @@ -338,7 +338,7 @@ namespace Barotrauma public Item FindItem(Func predicate, bool recursive) { - Item match = Items.FirstOrDefault(predicate); + Item match = Items.FirstOrDefault(i => i != null && predicate(i)); if (match == null && recursive) { foreach (var item in Items) @@ -360,13 +360,13 @@ namespace Barotrauma public Item FindItemByTag(string tag, bool recursive = false) { if (tag == null) { return null; } - return FindItem(i => i != null && i.HasTag(tag), recursive); + return FindItem(i => i.HasTag(tag), recursive); } public Item FindItemByIdentifier(string identifier, bool recursive = false) { if (identifier == null) return null; - return FindItem(i => i != null && i.Prefab.Identifier == identifier, recursive); + return FindItem(i => i.Prefab.Identifier == identifier, recursive); } public virtual void RemoveItem(Item item) diff --git a/Barotrauma/BarotraumaShared/Source/Items/Item.cs b/Barotrauma/BarotraumaShared/Source/Items/Item.cs index f45ee95c7..02340e7d8 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Item.cs @@ -59,6 +59,8 @@ namespace Barotrauma public readonly XElement StaticBodyConfig; + private bool transformDirty = true; + private float lastSentCondition; private float sendConditionUpdateTimer; private bool conditionUpdatePending; @@ -465,7 +467,7 @@ namespace Barotrauma { get; private set; - } = new List(); + } = new List(20); public string ConfigFile { @@ -578,7 +580,7 @@ namespace Barotrauma SerializableProperties = SerializableProperty.DeserializeProperties(this, element); - if (submarine == null || !submarine.Loading) FindHull(); + if (submarine == null || !submarine.Loading) { FindHull(); } SetActiveSprite(); @@ -588,8 +590,35 @@ namespace Barotrauma { case "body": body = new PhysicsBody(subElement, ConvertUnits.ToSimUnits(Position), Scale); - body.FarseerBody.AngularDamping = 0.2f; - body.FarseerBody.LinearDamping = 0.1f; + string collisionCategory = subElement.GetAttributeString("collisioncategory", null); + if (Prefab.DamagedByProjectiles || Prefab.DamagedByMeleeWeapons) + { + //force collision category to Character to allow projectiles and weapons to hit + //(we could also do this by making the projectiles and weapons hit CollisionItem + //and check if the collision should be ignored in the OnCollision callback, but + //that'd make the hit detection more expensive because every item would be included) + body.CollisionCategories = Physics.CollisionCharacter; + body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform | Physics.CollisionProjectile; + } + if (collisionCategory != null) + { + if (!Physics.TryParseCollisionCategory(collisionCategory, out Category cat)) + { + DebugConsole.ThrowError("Invalid collision category in item \"" + Name+"\" (" + collisionCategory + ")"); + } + else + { + body.CollisionCategories = cat; + if (cat.HasFlag(Physics.CollisionCharacter)) + { + body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform | Physics.CollisionProjectile; + } + } + } + + body.FarseerBody.AngularDamping = element.GetAttributeFloat("angulardamping", 0.2f); + body.FarseerBody.LinearDamping = element.GetAttributeFloat("lineardamping", 0.1f); + body.UserData = this; break; case "trigger": case "inventoryicon": @@ -874,7 +903,7 @@ namespace Barotrauma rect.X = (int)(displayPos.X - rect.Width / 2.0f); rect.Y = (int)(displayPos.Y + rect.Height / 2.0f); - if (findNewHull) FindHull(); + if (findNewHull) { FindHull(); } } public void SetActiveSprite() @@ -966,6 +995,8 @@ namespace Barotrauma return rootContainer; } + public bool IsOwnedBy(Character character) => FindParentInventory(i => i.Owner == character) != null; + public Inventory FindParentInventory(Func predicate) { if (parentInventory != null) @@ -1214,11 +1245,13 @@ namespace Barotrauma } } + if (Removed) { return; } + if (body != null && body.Enabled) { System.Diagnostics.Debug.Assert(body.FarseerBody.FixtureList != null); - if (Math.Abs(body.LinearVelocity.X) > 0.01f || Math.Abs(body.LinearVelocity.Y) > 0.01f) + if (Math.Abs(body.LinearVelocity.X) > 0.01f || Math.Abs(body.LinearVelocity.Y) > 0.01f || transformDirty) { UpdateTransform(); if (CurrentHull == null && body.SimPosition.Y < ConvertUnits.ToSimUnits(Level.MaxEntityDepth)) @@ -1255,6 +1288,8 @@ namespace Barotrauma public void UpdateTransform() { + if (body == null) { return; } + Submarine prevSub = Submarine; FindHull(); @@ -1283,6 +1318,8 @@ namespace Barotrauma MathHelper.Clamp(body.LinearVelocity.X, -NetConfig.MaxPhysicsBodyVelocity, NetConfig.MaxPhysicsBodyVelocity), MathHelper.Clamp(body.LinearVelocity.Y, -NetConfig.MaxPhysicsBodyVelocity, NetConfig.MaxPhysicsBodyVelocity)); } + + transformDirty = false; } /// @@ -1321,6 +1358,8 @@ namespace Barotrauma private bool OnCollision(Fixture f1, Fixture f2, Contact contact) { + if (transformDirty) { return false; } + Vector2 normal = contact.Manifold.LocalNormal; float impact = Vector2.Dot(f1.Body.LinearVelocity, -normal); @@ -1503,34 +1542,37 @@ namespace Barotrauma } } } - - + public void SendSignal(int stepsTaken, string signal, string connectionName, Character sender, float power = 0.0f, Item source = null, float signalStrength = 1.0f) { - LastSentSignalRecipients.Clear(); if (connections == null) { return; } + if (!connections.TryGetValue(connectionName, out Connection c)) { return; } + SendSignal(stepsTaken, signal, c, sender, power, source, signalStrength); + } + + public void SendSignal(int stepsTaken, string signal, Connection connection, Character sender, float power = 0.0f, Item source = null, float signalStrength = 1.0f) + { + LastSentSignalRecipients.Clear(); + if (connections == null || connection == null) { return; } stepsTaken++; - - if (!connections.TryGetValue(connectionName, out Connection c)) { return; } - + if (stepsTaken > 10) { //use a coroutine to prevent infinite loops by creating a one //frame delay if the "signal chain" gets too long - CoroutineManager.StartCoroutine(SendSignal(signal, c, sender, power, signalStrength)); + CoroutineManager.StartCoroutine(SendSignal(signal, connection, sender, power, signalStrength)); } else { - foreach (StatusEffect effect in c.Effects) + foreach (StatusEffect effect in connection.Effects) { if (condition <= 0.0f && effect.type != ActionType.OnBroken) { continue; } if (signal != "0" && !string.IsNullOrEmpty(signal)) { ApplyStatusEffect(effect, ActionType.OnUse, (float)Timing.Step, null, null, false, false); } } - c.SendSignal(stepsTaken, signal, source ?? this, sender, power, signalStrength); - } + connection.SendSignal(stepsTaken, signal, source ?? this, sender, power, signalStrength); + } } - private IEnumerable SendSignal(string signal, Connection connection, Character sender, float power = 0.0f, float signalStrength = 1.0f) { //wait one frame @@ -1854,7 +1896,6 @@ namespace Barotrauma foreach (ItemComponent ic in components) ic.Unequip(character); } - public List> GetProperties() { List> allProperties = new List>(); diff --git a/Barotrauma/BarotraumaShared/Source/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/Source/Items/ItemPrefab.cs index e5d3dc565..ebd60e015 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/ItemPrefab.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using System.IO; using System.Xml.Linq; using System.Linq; +using Barotrauma.Items.Components; +using Barotrauma.Extensions; namespace Barotrauma { @@ -247,6 +249,27 @@ namespace Barotrauma private set; } + [Serialize(false, false)] + public bool DamagedByExplosions + { + get; + private set; + } + + [Serialize(false, false)] + public bool DamagedByProjectiles + { + get; + private set; + } + + [Serialize(false, false)] + public bool DamagedByMeleeWeapons + { + get; + private set; + } + [Serialize(false, false)] public bool FireProof { @@ -310,6 +333,17 @@ namespace Barotrauma private set; } + private HashSet preferredContainers = new HashSet(); + [Serialize("", true, description: "Define containers (by identifiers or tags) that this item should be placed in. These are preferences, which are not enforced.")] + public string PreferredContainers + { + get { return string.Join(",", preferredContainers); } + set + { + StringFormatter.ParseCommaSeparatedStringToCollection(value, preferredContainers); + } + } + /// /// How likely it is for the item to spawn in a level of a given type. /// Key = name of the LevelGenerationParameters (empty string = default value) @@ -794,9 +828,24 @@ namespace Barotrauma } return prefab; } + public IEnumerable GetPrices() { return prices?.Values; } + + public bool IsContainerPreferred(ItemContainer itemContainer, out bool isPreferencesDefined) + { + isPreferencesDefined = preferredContainers.Any(); + if (!isPreferencesDefined) { return true; } + return preferredContainers.Any(id => itemContainer.Item.Prefab.Identifier == id || itemContainer.Item.HasTag(id)); + } + + public bool IsContainerPreferred(string[] identifiersOrTags, out bool isPreferencesDefined) + { + isPreferencesDefined = preferredContainers.Any(); + if (!isPreferencesDefined) { return true; } + return preferredContainers.Any(id => preferredContainers.Any(p => p == id)); + } } } diff --git a/Barotrauma/BarotraumaShared/Source/Items/RelatedItem.cs b/Barotrauma/BarotraumaShared/Source/Items/RelatedItem.cs index 739fdf82b..9af0fd2a2 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/RelatedItem.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/RelatedItem.cs @@ -98,10 +98,11 @@ namespace Barotrauma if (parentItem == null) { return false; } return CheckContained(parentItem); case RelationType.Container: - if (parentItem == null || parentItem.Container == null) { return false; } + if (parentItem == null || parentItem.Container == null) { return MatchOnEmpty; } return parentItem.Container.Condition > 0.0f && MatchesItem(parentItem.Container); case RelationType.Equipped: if (character == null) { return false; } + if (MatchOnEmpty && character.SelectedItems.All(it => it == null)) { return true; } foreach (Item equippedItem in character.SelectedItems) { if (equippedItem == null) { continue; } @@ -158,7 +159,7 @@ namespace Barotrauma if (!string.IsNullOrWhiteSpace(Msg)) element.Add(new XAttribute("msg", Msg)); } - public static RelatedItem Load(XElement element, string parentDebugName) + public static RelatedItem Load(XElement element, bool returnEmpty, string parentDebugName) { string[] identifiers; if (element.Attribute("name") != null) @@ -205,10 +206,9 @@ namespace Barotrauma } } - if (identifiers.Length == 0 && excludedIdentifiers.Length == 0) { return null; } + if (identifiers.Length == 0 && excludedIdentifiers.Length == 0 && !returnEmpty) { return null; } RelatedItem ri = new RelatedItem(identifiers, excludedIdentifiers); - string typeStr = element.GetAttributeString("type", ""); if (string.IsNullOrEmpty(typeStr)) { diff --git a/Barotrauma/BarotraumaShared/Source/Map/Entity.cs b/Barotrauma/BarotraumaShared/Source/Map/Entity.cs index 60ff02152..7ecc8e61b 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Entity.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Entity.cs @@ -110,9 +110,17 @@ namespace Barotrauma get { return aiTarget; } } + public double SpawnTime + { + get { return spawnTime; } + } + + private readonly double spawnTime; + public Entity(Submarine submarine) { this.Submarine = submarine; + spawnTime = Timing.TotalTime; //give a unique ID id = this is EntitySpawner ? diff --git a/Barotrauma/BarotraumaShared/Source/Map/Explosion.cs b/Barotrauma/BarotraumaShared/Source/Map/Explosion.cs index 5b16fcfe7..fc99bb304 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Explosion.cs @@ -124,39 +124,55 @@ namespace Barotrauma if (powerContainer != null) { powerContainer.Charge -= powerContainer.Capacity * empStrength * distFactor; - } + } } } - if (force == 0.0f && attack.Stun == 0.0f && attack.GetTotalDamage(false) == 0.0f) return; + if (MathUtils.NearlyEqual(force, 0.0f) && MathUtils.NearlyEqual(attack.Stun, 0.0f) && MathUtils.NearlyEqual(attack.GetTotalDamage(false), 0.0f)) + { + return; + } DamageCharacters(worldPosition, attack, force, damageSource, attacker); - + if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) { if (flames) { foreach (Item item in Item.ItemList) { - if (item.CurrentHull != hull || item.FireProof || item.Condition <= 0.0f) continue; + if (item.CurrentHull != hull || item.FireProof || item.Condition <= 0.0f) { continue; } //don't apply OnFire effects if the item is inside a fireproof container //(or if it's inside a container that's inside a fireproof container, etc) Item container = item.Container; + bool fireProof = false; while (container != null) { - if (container.FireProof) return; + if (container.FireProof) { fireProof = true; break; } container = container.Container; } - if (Vector2.Distance(item.WorldPosition, worldPosition) > attack.Range * 0.1f) continue; + if (fireProof || Vector2.Distance(item.WorldPosition, worldPosition) > attack.Range * 0.5f) { continue; } item.ApplyStatusEffects(ActionType.OnFire, 1.0f); - if (item.Condition <= 0.0f && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFire }); } + + if (item.Prefab.DamagedByExplosions && !item.Indestructible) + { + float limbRadius = item.body == null ? 0.0f : item.body.GetMaxExtent(); + float dist = Vector2.Distance(item.WorldPosition, worldPosition); + dist = Math.Max(0.0f, dist - ConvertUnits.ToDisplayUnits(limbRadius)); + + if (dist > attack.Range) { continue; } + + float distFactor = 1.0f - dist / attack.Range; + float damageAmount = attack.GetItemDamage(1.0f); + item.Condition -= damageAmount * distFactor; + } } } } @@ -197,7 +213,7 @@ namespace Barotrauma //calculate distance from the "outer surface" of the physics body //doesn't take the rotation of the limb into account, but should be accurate enough for this purpose float limbRadius = Math.Max(Math.Max(limb.body.width * 0.5f, limb.body.height * 0.5f), limb.body.radius); - dist = Math.Max(0.0f, dist - FarseerPhysics.ConvertUnits.ToDisplayUnits(limbRadius)); + dist = Math.Max(0.0f, dist - ConvertUnits.ToDisplayUnits(limbRadius)); if (dist > attack.Range) { continue; } @@ -240,7 +256,7 @@ namespace Barotrauma } } - if (limb.WorldPosition != worldPosition && force > 0.0f) + if (limb.WorldPosition != worldPosition && !MathUtils.NearlyEqual(force, 0.0f)) { Vector2 limbDiff = Vector2.Normalize(limb.WorldPosition - worldPosition); if (!MathUtils.IsValid(limbDiff)) limbDiff = Rand.Vector(1.0f); diff --git a/Barotrauma/BarotraumaShared/Source/Map/Gap.cs b/Barotrauma/BarotraumaShared/Source/Map/Gap.cs index d9a073f30..2008acac1 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Gap.cs @@ -55,6 +55,8 @@ namespace Barotrauma set { open = MathHelper.Clamp(value, 0.0f, 1.0f); } } + public float Size => IsHorizontal ? Rect.Height : Rect.Width; + public Door ConnectedDoor; public Structure ConnectedWall; diff --git a/Barotrauma/BarotraumaShared/Source/Map/Hull.cs b/Barotrauma/BarotraumaShared/Source/Map/Hull.cs index 602c23f6c..8d6fb6e0f 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Hull.cs @@ -228,13 +228,16 @@ namespace Barotrauma surface = rect.Y - rect.Height; - aiTarget = new AITarget(this) + if (submarine != null) { - MinSightRange = 2000, - MaxSightRange = 5000, - MaxSoundRange = 5000, - SoundRange = 0 - }; + aiTarget = new AITarget(this) + { + MinSightRange = 2000, + MaxSightRange = 5000, + MaxSoundRange = 5000, + SoundRange = 0 + }; + } hullList.Add(this); @@ -430,8 +433,11 @@ namespace Barotrauma FireSource.UpdateAll(FireSources, deltaTime); - aiTarget.SightRange = Submarine == null ? aiTarget.MinSightRange : Submarine.Velocity.Length() / 2 * aiTarget.MaxSightRange; - aiTarget.SoundRange -= deltaTime * 1000.0f; + if (aiTarget != null) + { + aiTarget.SightRange = Submarine == null ? aiTarget.MinSightRange : Submarine.Velocity.Length() / 2 * aiTarget.MaxSightRange; + aiTarget.SoundRange -= deltaTime * 1000.0f; + } if (!update) { @@ -594,16 +600,17 @@ namespace Barotrauma { adjacentHulls.Clear(); int startStep = 0; - return GetAdjacentHulls(includingThis, adjacentHulls, ref startStep, searchDepth); + searchDepth = searchDepth ?? 100; + return GetAdjacentHulls(includingThis, adjacentHulls, ref startStep, searchDepth.Value); } - private HashSet GetAdjacentHulls(bool includingThis, HashSet connectedHulls, ref int step, int? searchDepth) + private HashSet GetAdjacentHulls(bool includingThis, HashSet connectedHulls, ref int step, int searchDepth) { if (includingThis) { connectedHulls.Add(this); } - if (step > searchDepth.Value) + if (step > searchDepth) { return connectedHulls; } @@ -642,7 +649,7 @@ namespace Barotrauma foreach (Gap g in ConnectedGaps) { - if (g.ConnectedDoor != null) + if (g.ConnectedDoor != null && !g.ConnectedDoor.IsBroken) { //gap blocked if the door is not open or the predicted state is not open if (!g.ConnectedDoor.IsOpen || (g.ConnectedDoor.PredictedState.HasValue && !g.ConnectedDoor.PredictedState.Value)) @@ -660,7 +667,7 @@ namespace Barotrauma if (g.linkedTo[i] is Hull hull && !connectedHulls.Contains(hull)) { float dist = hull.GetApproximateHullDistance(g.Position, endPos, connectedHulls, target, distance + Vector2.Distance(startPos, g.Position), maxDistance); - if (dist < float.MaxValue) return dist; + if (dist < float.MaxValue) { return dist; } } } } diff --git a/Barotrauma/BarotraumaShared/Source/Map/Levels/Ruins/BTRoom.cs b/Barotrauma/BarotraumaShared/Source/Map/Levels/Ruins/BTRoom.cs index 4d400ba24..1537134e1 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Levels/Ruins/BTRoom.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Levels/Ruins/BTRoom.cs @@ -40,12 +40,24 @@ namespace Barotrauma.RuinGeneration this.rect = rect; } - public void Split(float minDivRatio, float verticalProbability = 0.5f, int minWidth = 200) + public void Split(float minDivRatio, float verticalProbability = 0.5f, int minWidth = 200, int minHeight = 200) { - subRooms = new BTRoom[2]; + bool verticalSplit = Rand.Range(0.0f, rect.Height / (float)rect.Width, Rand.RandSync.Server) < verticalProbability; + if (rect.Width * minDivRatio < minWidth && rect.Height * minDivRatio < minHeight) + { + minDivRatio = 0.5f; + } + else if (rect.Width * minDivRatio < minWidth) + { + verticalSplit = false; + } + else if (rect.Height * minDivRatio < minHeight) + { + verticalSplit = true; + } - if (Rand.Range(0.0f, rect.Height / (float)rect.Width, Rand.RandSync.Server) < verticalProbability && - rect.Width * minDivRatio >= minWidth) + subRooms = new BTRoom[2]; + if (verticalSplit) { SplitVertical(minDivRatio); } diff --git a/Barotrauma/BarotraumaShared/Source/Map/Levels/Ruins/RuinGenerationParams.cs b/Barotrauma/BarotraumaShared/Source/Map/Levels/Ruins/RuinGenerationParams.cs index bf80c5fa6..3fce2e3d9 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Levels/Ruins/RuinGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Levels/Ruins/RuinGenerationParams.cs @@ -69,12 +69,18 @@ namespace Barotrauma.RuinGeneration set; } - [Serialize(400, false, description: "The splitting algorithm attempts to keep the dimensions the split areas larger than this. For example, if the width of the split areas would be smaller than this after a vertical split, the algorithm will do a horizontal split."), Editable] + [Serialize(400, false, description: "The splitting algorithm attempts to keep the width of the split areas larger than this. If the width of the split areas would be smaller than this after a vertical split, the algorithm would do a horizontal split."), Editable] public int MinSplitWidth { get; set; } + [Serialize(400, false, description: "The splitting algorithm attempts to keep the height of the split areas larger than this. If the height of the split areas would be smaller than this after a vertical split, the algorithm would do a horizontal split."), Editable] + public int MinSplitHeight + { + get; + set; + } [Serialize("0.5,0.9", false, description: "The minimum and maximum width of a room relative to the areas created by the split algorithm."), Editable] public Vector2 RoomWidthRange diff --git a/Barotrauma/BarotraumaShared/Source/Map/Levels/Ruins/RuinGenerator.cs b/Barotrauma/BarotraumaShared/Source/Map/Levels/Ruins/RuinGenerator.cs index 70813cd69..c99b50cdc 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Levels/Ruins/RuinGenerator.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Levels/Ruins/RuinGenerator.cs @@ -239,7 +239,7 @@ namespace Barotrauma.RuinGeneration for (int i = 0; i < iterations; i++) { - rooms.ForEach(l => l.Split(0.3f, verticalProbability, generationParams.MinSplitWidth)); + rooms.ForEach(l => l.Split(0.3f, verticalProbability, generationParams.MinSplitWidth, generationParams.MinSplitHeight)); rooms = baseRoom.GetLeaves(); } @@ -559,6 +559,11 @@ namespace Barotrauma.RuinGeneration foreach (MapEntity e in entities) { e.Move(doorOffset); + Door doorComponent = (e as Item)?.GetComponent(); + if (doorComponent != null && !entities.Contains(doorComponent.LinkedGap)) + { + doorComponent.LinkedGap.Move(doorOffset); + } } } } diff --git a/Barotrauma/BarotraumaShared/Source/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/Source/Map/Map/Location.cs index 04a108a94..9e1514c81 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Map/Location.cs @@ -43,7 +43,7 @@ namespace Barotrauma LocationConnection connection = Connections[(MissionsCompleted + i) % Connections.Count]; Location destination = connection.OtherLocation(this); - var mission = Mission.LoadRandom(new Location[] { this, destination }, rand, true, MissionType.Random, true); + var mission = Mission.LoadRandom(new Location[] { this, destination }, rand, true, MissionType.All, true); if (mission == null) { continue; } if (availableMissions.Any(m => m.Prefab == mission.Prefab)) { continue; } if (GameSettings.VerboseLogging && mission != null) @@ -65,7 +65,11 @@ namespace Barotrauma public int SelectedMissionIndex { - get { return availableMissions.IndexOf(SelectedMission); } + get + { + if (SelectedMission == null) { return -1; } + return availableMissions.IndexOf(SelectedMission); + } set { if (value < 0 || value >= AvailableMissions.Count()) diff --git a/Barotrauma/BarotraumaShared/Source/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/Source/Map/MapEntity.cs index 4532f68be..2f095d308 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/MapEntity.cs @@ -73,15 +73,7 @@ namespace Barotrauma return !DrawBelowWater; } } - - public virtual bool DrawDamageEffect - { - get - { - return false; - } - } - + public virtual bool Linkable { get { return false; } @@ -344,6 +336,7 @@ namespace Barotrauma structure.Update(deltaTime, cam); } + //update gaps in random order, because otherwise in rooms with multiple gaps //the water/air will always tend to flow through the first gap in the list, //which may lead to weird behavior like water draining down only through @@ -353,6 +346,7 @@ namespace Barotrauma gap.Update(deltaTime, cam); } + Powered.UpdatePower(deltaTime); foreach (Item item in Item.ItemList) { item.Update(deltaTime, cam); diff --git a/Barotrauma/BarotraumaShared/Source/Map/Md5Hash.cs b/Barotrauma/BarotraumaShared/Source/Map/Md5Hash.cs index f9e8be7e1..a441941dd 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Md5Hash.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Md5Hash.cs @@ -85,6 +85,7 @@ namespace Barotrauma public static string GetShortHash(string fullHash) { + if (string.IsNullOrEmpty(fullHash)) { return ""; } return fullHash.Length < 7 ? fullHash : fullHash.Substring(0, 7); } } diff --git a/Barotrauma/BarotraumaShared/Source/Map/Structure.cs b/Barotrauma/BarotraumaShared/Source/Map/Structure.cs index 4836c9c69..cc7ebc246 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Structure.cs @@ -121,7 +121,7 @@ namespace Barotrauma } } - public override bool DrawDamageEffect + public bool DrawDamageEffect { get { @@ -206,6 +206,14 @@ namespace Barotrauma } private Rectangle defaultRect; + /// + /// Unscaled rect + /// + public Rectangle DefaultRect + { + get { return defaultRect; } + set { defaultRect = value; } + } public override Rectangle Rect { diff --git a/Barotrauma/BarotraumaShared/Source/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/Source/Map/WayPoint.cs index aa833b8a8..82c473310 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/WayPoint.cs @@ -83,6 +83,11 @@ namespace Barotrauma } } + public JobPrefab AssignedJob + { + get { return assignedJob; } + } + public WayPoint(Vector2 position, SpawnType spawnType, Submarine submarine, Gap gap = null) : this(new Rectangle((int)position.X - 3, (int)position.Y + 3, 6, 6), submarine) { diff --git a/Barotrauma/BarotraumaShared/Source/Networking/ChatMessage.cs b/Barotrauma/BarotraumaShared/Source/Networking/ChatMessage.cs index 7add8f969..b2bc4db8f 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/ChatMessage.cs @@ -18,7 +18,9 @@ namespace Barotrauma.Networking public const int MaxMessagesPerPacket = 10; public const float SpeakRange = 2000.0f; - + + private static readonly string dateTimeFormatLongTimePattern = System.Globalization.CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern; + public static Color[] MessageColor = { new Color(190, 198, 205), //default @@ -66,6 +68,11 @@ namespace Barotrauma.Networking get { return MessageColor[(int)Type]; } } + public static string GetTimeStamp() + { + return $"[{DateTime.Now.ToString(dateTimeFormatLongTimePattern)}] "; + } + public string TextWithSender { get diff --git a/Barotrauma/BarotraumaShared/Source/Networking/Client.cs b/Barotrauma/BarotraumaShared/Source/Networking/Client.cs index 64888301a..98c06bf6d 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/Client.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/Client.cs @@ -14,6 +14,8 @@ namespace Barotrauma.Networking public byte ID; public UInt64 SteamID; + public string PreferredJob; + public Character.TeamType TeamID; private Character character; diff --git a/Barotrauma/BarotraumaShared/Source/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaShared/Source/Networking/EntitySpawner.cs index fff151815..50b186cd1 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/EntitySpawner.cs @@ -7,8 +7,6 @@ namespace Barotrauma { partial class EntitySpawner : Entity, IServerSerializable { - const int MaxEntitiesPerWrite = 10; - private enum SpawnableType { Item, Character }; interface IEntitySpawnInfo @@ -53,7 +51,7 @@ namespace Barotrauma { return null; } - Item spawnedItem = null; + Item spawnedItem; if (Inventory != null) { spawnedItem = new Item(Prefab, Vector2.Zero, null); @@ -67,6 +65,41 @@ namespace Barotrauma } } + class CharacterSpawnInfo : IEntitySpawnInfo + { + public readonly string identifier; + + public readonly Vector2 Position; + public readonly Submarine Submarine; + + private readonly Action onSpawn; + + public CharacterSpawnInfo(string identifier, Vector2 worldPosition, Action onSpawn = null) + { + this.identifier = identifier ?? throw new ArgumentException("ItemSpawnInfo prefab cannot be null."); + Position = worldPosition; + this.onSpawn = onSpawn; + } + + public CharacterSpawnInfo(string identifier, Vector2 position, Submarine sub, Action onSpawn = null) + { + this.identifier = identifier ?? throw new ArgumentException("ItemSpawnInfo prefab cannot be null."); + Position = position; + Submarine = sub; + this.onSpawn = onSpawn; + } + + public Entity Spawn() + { + var character = string.IsNullOrEmpty(identifier) ? null : + Character.Create(identifier, + Submarine == null ? Position : Submarine.Position + Position, + ToolBox.RandomSeed(8), createNetworkEvent: false); + onSpawn?.Invoke(character); + return character; + } + } + private readonly Queue spawnQueue; private readonly Queue removeQueue; @@ -134,6 +167,32 @@ namespace Barotrauma spawnQueue.Enqueue(new ItemSpawnInfo(itemPrefab, inventory, condition)); } + public void AddToSpawnQueue(string speciesName, Vector2 worldPosition, Action onSpawn = null) + { + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + if (string.IsNullOrEmpty(speciesName)) + { + string errorMsg = "Attempted to add an empty/null species name to entity spawn queue.\n" + Environment.StackTrace; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue4:SpeciesNameNullOrEmpty", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + return; + } + spawnQueue.Enqueue(new CharacterSpawnInfo(speciesName, worldPosition, onSpawn)); + } + + public void AddToSpawnQueue(string speciesName, Vector2 position, Submarine sub, Action onSpawn = null) + { + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + if (string.IsNullOrEmpty(speciesName)) + { + string errorMsg = "Attempted to add an empty/null species name to entity spawn queue.\n" + Environment.StackTrace; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("EntitySpawner.AddToSpawnQueue5:SpeciesNameNullOrEmpty", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + return; + } + spawnQueue.Enqueue(new CharacterSpawnInfo(speciesName, position, sub, onSpawn)); + } + public void AddToRemoveQueue(Entity entity) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } diff --git a/Barotrauma/BarotraumaShared/Source/Networking/NetConfig.cs b/Barotrauma/BarotraumaShared/Source/Networking/NetConfig.cs index 2df69075a..6fd92e83f 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/NetConfig.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/NetConfig.cs @@ -44,6 +44,7 @@ namespace Barotrauma.Networking public const float ItemConditionUpdateInterval = 0.15f; public const float LevelObjectUpdateInterval = 0.5f; public const float HullUpdateInterval = 0.5f; + public const float SparseHullUpdateInterval = 5.0f; public const float HullUpdateDistance = 20000.0f; public const int MaxEventPacketsPerUpdate = 4; diff --git a/Barotrauma/BarotraumaShared/Source/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/Source/Networking/NetworkMember.cs index 0bf51e210..056467f39 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/NetworkMember.cs @@ -62,7 +62,8 @@ namespace Barotrauma.Networking STARTGAME, //start a new round ENDGAME, - TRAITOR_MESSAGE + TRAITOR_MESSAGE, + MISSION } enum ServerNetObject { @@ -152,12 +153,10 @@ namespace Barotrauma.Networking protected RespawnManager respawnManager; public bool ShowNetStats; - -#if DEBUG + public float SimulatedRandomLatency, SimulatedMinimumLatency; public float SimulatedLoss; public float SimulatedDuplicatesChance; -#endif public int TickRate { diff --git a/Barotrauma/BarotraumaShared/Source/Networking/Primitives/NetworkConnection/LidgrenConnection.cs b/Barotrauma/BarotraumaShared/Source/Networking/Primitives/NetworkConnection/LidgrenConnection.cs index 1825dd108..8ab15b2e1 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/Primitives/NetworkConnection/LidgrenConnection.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/Primitives/NetworkConnection/LidgrenConnection.cs @@ -14,7 +14,7 @@ namespace Barotrauma.Networking { get { - return IPEndPoint.Address.IsIPv4MappedToIPv6 ? IPEndPoint.Address.MapToIPv4().ToString() : IPEndPoint.Address.ToString(); + return IPEndPoint.Address.IsIPv4MappedToIPv6 ? IPEndPoint.Address.MapToIPv4NoThrow().ToString() : IPEndPoint.Address.ToString(); } } diff --git a/Barotrauma/BarotraumaShared/Source/Networking/ServerLog.cs b/Barotrauma/BarotraumaShared/Source/Networking/ServerLog.cs index 02606590c..a5de5ef35 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/ServerLog.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/ServerLog.cs @@ -17,11 +17,11 @@ namespace Barotrauma.Networking { if (type.HasFlag(MessageType.Chat)) { - Text = $"[{DateTime.Now.ToString()}] {text}"; + Text = $"[{DateTime.Now.ToString()}]\n {text}"; } else { - Text = $"[{DateTime.Now.ToString()}] {TextManager.GetServerMessage(text)}"; + Text = $"[{DateTime.Now.ToString()}]\n {TextManager.GetServerMessage(text)}"; } Type = type; @@ -101,7 +101,7 @@ namespace Barotrauma.Networking lines.Enqueue(newText); #if CLIENT - if (LogFrame != null) + if (listBox != null) { AddLine(newText); diff --git a/Barotrauma/BarotraumaShared/Source/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/Source/Networking/ServerSettings.cs index 73776fb79..8670798c0 100644 --- a/Barotrauma/BarotraumaShared/Source/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/Source/Networking/ServerSettings.cs @@ -28,6 +28,15 @@ namespace Barotrauma.Networking Normal, Fill } + public enum PlayStyle + { + Serious = 0, + Casual = 1, + Roleplay = 2, + Rampage = 3, + SomethingDifferent = 4 + } + partial class ServerSettings : ISerializableEntity { public const string SettingsFile = "serversettings.xml"; @@ -82,10 +91,9 @@ namespace Barotrauma.Networking partial class NetPropertyData { - private SerializableProperty property; - private string typeString; - - private object parentObject; + private readonly SerializableProperty property; + private readonly string typeString; + private readonly object parentObject; public string Name { @@ -257,7 +265,7 @@ namespace Barotrauma.Networking private set; } - Dictionary netProperties; + private readonly Dictionary netProperties; partial void InitProjSpecific(); @@ -348,7 +356,6 @@ namespace Barotrauma.Networking public Dictionary ExtraCargo { get; private set; } - private TimeSpan sparseUpdateInterval = new TimeSpan(0, 0, 0, 3); private float selectedLevelDifficulty; private byte[] password; @@ -481,6 +488,18 @@ namespace Barotrauma.Networking } } + private PlayStyle playstyleSelection; + [Serialize(PlayStyle.Serious, true)] + public PlayStyle PlayStyle + { + get { return playstyleSelection; } + set + { + playstyleSelection = value; + ServerDetailsChanged = true; + } + } + [Serialize(800, true)] private int LinesPerLogFile { @@ -603,6 +622,20 @@ namespace Barotrauma.Networking set; } + + [Serialize("", true)] + public string SelectedSubmarine + { + get; + set; + } + [Serialize("", true)] + public string SelectedShuttle + { + get; + set; + } + private YesNoMaybe traitorsEnabled; [Serialize(YesNoMaybe.No, true)] public YesNoMaybe TraitorsEnabled @@ -744,7 +777,7 @@ namespace Barotrauma.Networking set; } - [Serialize("Random", true)] + [Serialize("All", true)] public string MissionType { get; diff --git a/Barotrauma/BarotraumaShared/Source/Physics/Physics.cs b/Barotrauma/BarotraumaShared/Source/Physics/Physics.cs index 392572daf..b19aa5e29 100644 --- a/Barotrauma/BarotraumaShared/Source/Physics/Physics.cs +++ b/Barotrauma/BarotraumaShared/Source/Physics/Physics.cs @@ -18,6 +18,51 @@ namespace Barotrauma public static float DisplayToRealWorldRatio = 1.0f / 80.0f; - public const float DisplayToSimRation = 100.0f; + public const float DisplayToSimRation = 100.0f; + + public static bool TryParseCollisionCategory(string categoryName, out Category category) + { + category = Category.None; + if (string.IsNullOrEmpty(categoryName)) + { + return false; + } + switch (categoryName.ToLowerInvariant()) + { + case "all": + category = CollisionAll; + return true; + case "wall": + case "structure": + category = CollisionWall; + return true; + case "character": + category = CollisionCharacter; + return true; + case "platform": + category = CollisionPlatform; + return true; + case "stairs": + category = CollisionStairs; + return true; + case "item": + category = CollisionItem; + return true; + case "itemblocking": + category = CollisionItemBlocking; + return true; + case "projectile": + category = CollisionProjectile; + return true; + case "level": + category = CollisionLevel; + return true; + case "repair": + category = CollisionRepair; + return true; + default: + return false; + } + } } } diff --git a/Barotrauma/BarotraumaShared/Source/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/Source/Physics/PhysicsBody.cs index 706d29e58..599924055 100644 --- a/Barotrauma/BarotraumaShared/Source/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/Source/Physics/PhysicsBody.cs @@ -445,7 +445,7 @@ namespace Barotrauma default: throw new NotImplementedException(); } - return spritesheetRotation == 0 ? pos : Vector2.Transform(pos, Matrix.CreateRotationZ(spritesheetRotation)); + return spritesheetRotation == 0 ? pos : Vector2.Transform(pos, Matrix.CreateRotationZ(-spritesheetRotation)); } public float GetMaxExtent() @@ -654,9 +654,9 @@ namespace Barotrauma if (newSpeedSqr > maxVelocity * maxVelocity) { newVelocity = newVelocity.ClampLength(maxVelocity); + force = (newVelocity - body.LinearVelocity) * Mass / (float)Timing.Step; } - Vector2 clampedForce = (newVelocity - body.LinearVelocity) * Mass / (float)Timing.Step; if (!IsValidValue(force, "clamped force", -1e10f, 1e10f)) return; body.ApplyForce(force); } diff --git a/Barotrauma/BarotraumaShared/Source/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaShared/Source/Screens/NetLobbyScreen.cs index e479fb5c6..4d5cfa806 100644 --- a/Barotrauma/BarotraumaShared/Source/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaShared/Source/Screens/NetLobbyScreen.cs @@ -33,6 +33,7 @@ namespace Barotrauma #endif #if CLIENT levelDifficultyScrollBar.BarScroll = difficulty / 100.0f; + levelDifficultyScrollBar.OnMoved(levelDifficultyScrollBar, levelDifficultyScrollBar.BarScroll); #endif } diff --git a/Barotrauma/BarotraumaShared/Source/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/Source/Serialization/SerializableProperty.cs index 56f502e56..75ce1f208 100644 --- a/Barotrauma/BarotraumaShared/Source/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/Source/Serialization/SerializableProperty.cs @@ -689,7 +689,8 @@ namespace Barotrauma { if (subElement.Name.ToString().ToLowerInvariant() != "upgrade") { continue; } var upgradeVersion = new Version(subElement.GetAttributeString("gameversion", "0.0.0.0")); - if (savedVersion >= upgradeVersion) { continue; } + if (savedVersion >= upgradeVersion) { continue; } + foreach (XAttribute attribute in subElement.Attributes()) { string attributeName = attribute.Name.ToString().ToLowerInvariant(); @@ -698,9 +699,9 @@ namespace Barotrauma { property.TrySetValue(entity, attribute.Value); } - else if (entity is Item item) + else if (entity is Item item1) { - foreach (ISerializableEntity component in item.AllPropertyObjects) + foreach (ISerializableEntity component in item1.AllPropertyObjects) { if (component.SerializableProperties.TryGetValue(attributeName, out SerializableProperty componentProperty)) { @@ -708,7 +709,28 @@ namespace Barotrauma } } } - } + } + + if (entity is Item item2) + { + XElement componentElement = subElement.FirstElement(); + if (componentElement == null) continue; + ItemComponent itemComponent = item2.Components.First(c => c.Name == componentElement.Name.ToString()); + if (itemComponent == null) continue; + foreach (XElement element in componentElement.Elements()) + { + switch (element.Name.ToString().ToLowerInvariant()) + { + case "requireditem": + case "requireditems": + itemComponent.requiredItems.Clear(); + itemComponent.DisabledRequiredItems.Clear(); + + itemComponent.SetRequiredItems(element); + break; + } + } + } } } } diff --git a/Barotrauma/BarotraumaShared/Source/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/Source/Serialization/XMLExtensions.cs index 4385a3427..fa5b9db8b 100644 --- a/Barotrauma/BarotraumaShared/Source/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/Source/Serialization/XMLExtensions.cs @@ -260,6 +260,42 @@ namespace Barotrauma return val; } + public static UInt64 GetAttributeUInt64(this XElement element, string name, UInt64 defaultValue) + { + if (element?.Attribute(name) == null) return defaultValue; + + UInt64 val = defaultValue; + + try + { + val = UInt64.Parse(element.Attribute(name).Value); + } + catch (Exception e) + { + DebugConsole.ThrowError("Error in " + element + "! ", e); + } + + return val; + } + + public static UInt64 GetAttributeSteamID(this XElement element, string name, UInt64 defaultValue) + { + if (element?.Attribute(name) == null) return defaultValue; + + UInt64 val = defaultValue; + + try + { + val = Steam.SteamManager.SteamIDStringToUInt64(element.Attribute(name).Value); + } + catch (Exception e) + { + DebugConsole.ThrowError("Error in " + element + "! ", e); + } + + return val; + } + public static int[] GetAttributeIntArray(this XElement element, string name, int[] defaultValue) { if (element?.Attribute(name) == null) return defaultValue; @@ -497,27 +533,54 @@ namespace Barotrauma Color color = Color.White; - if (strComponents.Length < 3) - { - if (errorMessages) DebugConsole.ThrowError("Failed to parse the string \"" + stringColor + "\" to Color"); - return Color.White; - } - float[] components = new float[4] { 1.0f, 1.0f, 1.0f, 1.0f }; - - for (int i = 0; i < 4 && i < strComponents.Length; i++) - { - float.TryParse(strComponents[i], NumberStyles.Float, CultureInfo.InvariantCulture, out components[i]); - } - if (components.Any(c => c > 1.0f)) + if (strComponents.Length == 1) { - for (int i = 0; i < 4; i++) + bool hexFailed = true; + stringColor = stringColor.Trim(); + if (stringColor[0]=='#') { - components[i] = components[i] / 255.0f; + stringColor = stringColor.Substring(1); + + int colorInt = 0; + if (int.TryParse(stringColor, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out colorInt)) + { + if (stringColor.Length == 6) + { + colorInt = (colorInt << 8) | 0xff; + } + components[0] = ((float)((colorInt & 0xff000000) >> 24)) / 255.0f; + components[1] = ((float)((colorInt & 0x00ff0000) >> 16)) / 255.0f; + components[2] = ((float)((colorInt & 0x0000ff00) >> 8)) / 255.0f; + components[3] = ((float)(colorInt & 0x000000ff)) / 255.0f; + + hexFailed = false; + } + } + + if (hexFailed) + { + if (errorMessages) DebugConsole.ThrowError("Failed to parse the string \"" + stringColor + "\" to Color"); + return Color.White; + } + } + else + { + for (int i = 0; i < 4 && i < strComponents.Length; i++) + { + float.TryParse(strComponents[i], NumberStyles.Float, CultureInfo.InvariantCulture, out components[i]); + } + + if (components.Any(c => c > 1.0f)) + { + for (int i = 0; i < 4; i++) + { + components[i] = components[i] / 255.0f; + } + //alpha defaults to 1.0 if not given + if (strComponents.Length < 4) components[3] = 1.0f; } - //alpha defaults to 255 if not given - if (strComponents.Length < 4) components[3] = 255; } return new Color(components[0], components[1], components[2], components[3]); diff --git a/Barotrauma/BarotraumaShared/Source/Sprite/ConditionalSprite.cs b/Barotrauma/BarotraumaShared/Source/Sprite/ConditionalSprite.cs index 93fa48eb7..f67db30b2 100644 --- a/Barotrauma/BarotraumaShared/Source/Sprite/ConditionalSprite.cs +++ b/Barotrauma/BarotraumaShared/Source/Sprite/ConditionalSprite.cs @@ -4,20 +4,34 @@ using System.Linq; namespace Barotrauma { - class ConditionalSprite : Sprite + partial class ConditionalSprite { public readonly List conditionals = new List(); public bool IsActive => Target != null && conditionals.All(c => c.Matches(Target)); - readonly ISerializableEntity Target; + public ISerializableEntity Target { get; private set; } + public Sprite Sprite { get; private set; } + public DeformableSprite DeformableSprite { get; private set; } + public Sprite ActiveSprite => Sprite ?? DeformableSprite.Sprite; - public ConditionalSprite(XElement element, ISerializableEntity target, string path = "", string file = "") : base(element, path, file) + public ConditionalSprite(XElement element, ISerializableEntity target, string path = "", string file = "", bool lazyLoad = false) { Target = target; foreach (XElement subElement in element.Elements()) { - foreach (XAttribute attribute in subElement.Attributes()) + switch (subElement.Name.ToString().ToLowerInvariant()) { - conditionals.Add(new PropertyConditional(attribute)); + case "conditional": + foreach (XAttribute attribute in subElement.Attributes()) + { + conditionals.Add(new PropertyConditional(attribute)); + } + break; + case "sprite": + Sprite = new Sprite(subElement, path, file, lazyLoad: lazyLoad); + break; + case "deformablesprite": + DeformableSprite = new DeformableSprite(subElement, filePath: path, lazyLoad: lazyLoad); + break; } } } diff --git a/Barotrauma/BarotraumaShared/Source/StatusEffects/DelayedEffect.cs b/Barotrauma/BarotraumaShared/Source/StatusEffects/DelayedEffect.cs index 354c1b2bc..cddc9c97d 100644 --- a/Barotrauma/BarotraumaShared/Source/StatusEffects/DelayedEffect.cs +++ b/Barotrauma/BarotraumaShared/Source/StatusEffects/DelayedEffect.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Microsoft.Xna.Framework; namespace Barotrauma { @@ -8,6 +9,7 @@ namespace Barotrauma { public DelayedEffect Parent; public Entity Entity; + public Vector2? WorldPosition; public List Targets; public float StartTimer; } @@ -23,7 +25,7 @@ namespace Barotrauma delay = element.GetAttributeFloat("delay", 1.0f); } - public override void Apply(ActionType type, float deltaTime, Entity entity, ISerializableEntity target) + public override void Apply(ActionType type, float deltaTime, Entity entity, ISerializableEntity target, Vector2? worldPosition = null) { if (this.type != type || !HasRequiredItems(entity)) return; if (!Stackable && DelayList.Any(d => d.Parent == this && d.Targets.FirstOrDefault() == target)) return; @@ -36,13 +38,14 @@ namespace Barotrauma Parent = this, StartTimer = delay, Entity = entity, + WorldPosition = worldPosition, Targets = new List() { target } }; DelayList.Add(element); } - public override void Apply(ActionType type, float deltaTime, Entity entity, IEnumerable targets) + public override void Apply(ActionType type, float deltaTime, Entity entity, IEnumerable targets, Vector2? worldPosition = null) { if (this.type != type || !HasRequiredItems(entity)) return; if (!Stackable && DelayList.Any(d => d.Parent == this && d.Targets.SequenceEqual(targets))) return; @@ -65,6 +68,7 @@ namespace Barotrauma Parent = this, StartTimer = delay, Entity = entity, + WorldPosition = worldPosition, Targets = currentTargets }; @@ -86,7 +90,7 @@ namespace Barotrauma if (element.StartTimer > 0.0f) continue; - element.Parent.Apply(1.0f, element.Entity, element.Targets); + element.Parent.Apply(1.0f, element.Entity, element.Targets, element.WorldPosition); DelayList.Remove(element); } } diff --git a/Barotrauma/BarotraumaShared/Source/StatusEffects/PropertyConditional.cs b/Barotrauma/BarotraumaShared/Source/StatusEffects/PropertyConditional.cs index 4675e0194..c33a5238e 100644 --- a/Barotrauma/BarotraumaShared/Source/StatusEffects/PropertyConditional.cs +++ b/Barotrauma/BarotraumaShared/Source/StatusEffects/PropertyConditional.cs @@ -250,29 +250,32 @@ namespace Barotrauma } case ConditionType.Affliction: if (target == null) { return Operator == OperatorType.NotEquals; } - if (target is Character targetChar) + + Character targetChar = target as Character; + if (target is Limb limb) { targetChar = limb.character; } + if (targetChar != null) { var health = targetChar.CharacterHealth; if (health == null) { return false; } var affliction = health.GetAffliction(AttributeName); - if (affliction == null) { return false; } + float afflictionStrength = affliction == null ? 0.0f : affliction.Strength; if (FloatValue.HasValue) { float value = FloatValue.Value; switch (Operator) { case OperatorType.Equals: - return affliction.Strength == value; + return afflictionStrength == value; case OperatorType.GreaterThan: - return affliction.Strength > value; + return afflictionStrength > value; case OperatorType.GreaterThanEquals: - return affliction.Strength >= value; + return afflictionStrength >= value; case OperatorType.LessThan: - return affliction.Strength < value; + return afflictionStrength < value; case OperatorType.LessThanEquals: - return affliction.Strength <= value; + return afflictionStrength <= value; case OperatorType.NotEquals: - return affliction.Strength != value; + return afflictionStrength != value; } } } diff --git a/Barotrauma/BarotraumaShared/Source/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/Source/StatusEffects/StatusEffect.cs index 79ee3e828..1cd61bd4f 100644 --- a/Barotrauma/BarotraumaShared/Source/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/Source/StatusEffects/StatusEffect.cs @@ -87,6 +87,27 @@ namespace Barotrauma } } + class CharacterSpawnInfo : ISerializableEntity + { + public string Name => $"Character Spawn Info ({SpeciesName})"; + public Dictionary SerializableProperties { get; set; } + + [Serialize("", false)] + public string SpeciesName { get; private set; } + [Serialize(1, false)] + public int Count { get; private set; } + [Serialize(0f, false)] + public float Spread { get; private set; } + + public CharacterSpawnInfo(XElement element, string parentDebugName) + { + SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + if (string.IsNullOrEmpty(SpeciesName)) + { + DebugConsole.ThrowError($"Invalid character spawn ({Name}) in StatusEffect \"{parentDebugName}\" - identifier not found in the element \"{element.ToString()}\""); + } + } + } private TargetType targetTypes; protected HashSet targetIdentifiers; @@ -106,6 +127,9 @@ namespace Barotrauma private HashSet tags; private readonly float duration; + private readonly float lifeTime; + private float lifeTimer; + public static readonly List DurationList = new List(); public bool CheckConditionalAlways; //Always do the conditional checks for the duration/delay. If false, only check conditional on apply. @@ -114,17 +138,20 @@ namespace Barotrauma private readonly int useItemCount; - private readonly bool removeItem; + private readonly bool removeItem, removeCharacter; public readonly ActionType type = ActionType.OnActive; - private Explosion explosion; + private readonly Explosion explosion; private List spawnItems; + private List spawnCharacters; private Character user; public readonly float FireSize; + + public readonly LimbType targetLimb; public readonly float SeverLimbsProbability; @@ -180,11 +207,17 @@ namespace Barotrauma { requiredItems = new List(); spawnItems = new List(); + spawnCharacters = new List(); Afflictions = new List(); ReduceAffliction = new List>(); tags = new HashSet(element.GetAttributeString("tags", "").Split(',')); Range = element.GetAttributeFloat("range", 0.0f); + string targetLimbName = element.GetAttributeString("targetlimb", null); + if (targetLimbName != null) + { + Enum.TryParse(targetLimbName, out targetLimb); + } IEnumerable attributes = element.Attributes(); List propertyAttributes = new List(); @@ -242,6 +275,10 @@ namespace Barotrauma case "stackable": Stackable = attribute.GetAttributeBool(true); break; + case "lifetime": + lifeTime = attribute.GetAttributeFloat(0); + lifeTimer = lifeTime; + break; case "checkconditionalalways": CheckConditionalAlways = attribute.GetAttributeBool(false); break; @@ -292,9 +329,12 @@ namespace Barotrauma case "removeitem": removeItem = true; break; + case "removecharacter": + removeCharacter = true; + break; case "requireditem": case "requireditems": - RelatedItem newRequiredItem = RelatedItem.Load(subElement, parentDebugName); + RelatedItem newRequiredItem = RelatedItem.Load(subElement, returnEmpty: false, parentDebugName: parentDebugName); if (newRequiredItem == null) { DebugConsole.ThrowError("Error in StatusEffect config - requires an item with no identifiers."); @@ -365,7 +405,11 @@ namespace Barotrauma break; case "spawnitem": var newSpawnItem = new ItemSpawnInfo(subElement, parentDebugName); - if (newSpawnItem.ItemPrefab != null) spawnItems.Add(newSpawnItem); + if (newSpawnItem.ItemPrefab != null) { spawnItems.Add(newSpawnItem); } + break; + case "spawncharacter": + var newSpawnCharacter = new CharacterSpawnInfo(subElement, parentDebugName); + if (!string.IsNullOrWhiteSpace(newSpawnCharacter.SpeciesName)) { spawnCharacters.Add(newSpawnCharacter); } break; } } @@ -433,8 +477,8 @@ namespace Barotrauma public virtual bool HasRequiredConditions(List targets) { - if (!propertyConditionals.Any()) return true; - if (requiredItems.All(ri => ri.MatchOnEmpty) && targets.Count == 0) return true; + if (!propertyConditionals.Any()) { return true; } + if (requiredItems.Any() && requiredItems.All(ri => ri.MatchOnEmpty) && targets.Count == 0) { return true; } switch (conditionalComparison) { case PropertyConditional.Comparison.Or: @@ -513,7 +557,7 @@ namespace Barotrauma } } - public virtual void Apply(ActionType type, float deltaTime, Entity entity, ISerializableEntity target) + public virtual void Apply(ActionType type, float deltaTime, Entity entity, ISerializableEntity target, Vector2? worldPosition = null) { if (this.type != type || !HasRequiredItems(entity)) return; @@ -535,11 +579,11 @@ namespace Barotrauma if (!HasRequiredConditions(targets)) return; - Apply(deltaTime, entity, targets); + Apply(deltaTime, entity, targets, worldPosition); } protected readonly List currentTargets = new List(); - public virtual void Apply(ActionType type, float deltaTime, Entity entity, IEnumerable targets) + public virtual void Apply(ActionType type, float deltaTime, Entity entity, IEnumerable targets, Vector2? worldPosition = null) { if (this.type != type) return; @@ -570,11 +614,17 @@ namespace Barotrauma } } - Apply(deltaTime, entity, currentTargets); + Apply(deltaTime, entity, currentTargets, worldPosition); } - protected void Apply(float deltaTime, Entity entity, List targets) + protected void Apply(float deltaTime, Entity entity, List targets, Vector2? worldPosition = null) { + if (lifeTime > 0) + { + lifeTimer -= deltaTime; + if (lifeTimer <= 0) { return; } + } + Hull hull = null; if (entity is Character) { @@ -585,9 +635,22 @@ namespace Barotrauma hull = ((Item)entity).CurrentHull; } + Vector2 position = worldPosition ?? entity.WorldPosition; + if (targetLimb != LimbType.None) + { + if (entity is Character c) + { + Limb limb = c.AnimController.GetLimb(targetLimb); + if (limb != null) + { + position = limb.WorldPosition; + } + } + } + foreach (ISerializableEntity serializableEntity in targets) { - if (!(serializableEntity is Item item)) continue; + if (!(serializableEntity is Item item)) { continue; } Character targetCharacter = targets.FirstOrDefault(t => t is Character character && !character.Removed) as Character; if (targetCharacter == null) @@ -606,9 +669,16 @@ namespace Barotrauma if (removeItem) { - foreach (Item item in targets.Where(t => t is Item).Cast()) + foreach (var target in targets) { - Entity.Spawner?.AddToRemoveQueue(item); + if (target is Item item) { Entity.Spawner?.AddToRemoveQueue(item); } + } + } + if (removeCharacter) + { + foreach (var target in targets) + { + if (target is Character character) { Entity.Spawner?.AddToRemoveQueue(character); } } } @@ -643,7 +713,10 @@ namespace Barotrauma } } - if (explosion != null && entity != null) { explosion.Explode(entity.WorldPosition, damageSource: entity, attacker: user); } + if (explosion != null && entity != null) + { + explosion.Explode(position, damageSource: entity, attacker: user); + } foreach (ISerializableEntity target in targets) { @@ -658,7 +731,7 @@ namespace Barotrauma character.LastDamageSource = entity; foreach (Limb limb in character.AnimController.Limbs) { - limb.character.DamageLimb(entity.WorldPosition, limb, new List() { multipliedAffliction }, stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source); + limb.character.DamageLimb(position, limb, new List() { multipliedAffliction }, stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source); limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability); //only apply non-limb-specific afflictions to the first limb if (!affliction.Prefab.LimbSpecific) { break; } @@ -667,7 +740,7 @@ namespace Barotrauma else if (target is Limb limb) { if (limb.character.Removed) { continue; } - limb.character.DamageLimb(entity.WorldPosition, limb, new List() { multipliedAffliction }, stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source); + limb.character.DamageLimb(position, limb, new List() { multipliedAffliction }, stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source); limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability); } } @@ -694,25 +767,40 @@ namespace Barotrauma GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, user, prevVitality - targetCharacter.Vitality); #endif } - } } if (FireSize > 0.0f && entity != null) { - var fire = new FireSource(entity.WorldPosition, hull); + var fire = new FireSource(position, hull); fire.Size = new Vector2(FireSize, fire.Size.Y); } bool isNotClient = GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient; - if (isNotClient && entity != null && Entity.Spawner != null) //clients are not allowed to spawn items + if (isNotClient && entity != null && Entity.Spawner != null) //clients are not allowed to spawn entities { + foreach (CharacterSpawnInfo characterSpawnInfo in spawnCharacters) + { + var characters = new List(); + for (int i = 0; i < characterSpawnInfo.Count; i++) + { + Entity.Spawner.AddToSpawnQueue(characterSpawnInfo.SpeciesName, position + Rand.Vector(characterSpawnInfo.Spread, Rand.RandSync.Server), + onSpawn: newCharacter => + { + characters.Add(newCharacter); + if (characters.Count == characterSpawnInfo.Count) + { + SwarmBehavior.CreateSwarm(characters.Cast()); + } + }); + } + } foreach (ItemSpawnInfo itemSpawnInfo in spawnItems) { switch (itemSpawnInfo.SpawnPosition) { case ItemSpawnInfo.SpawnPositionType.This: - Entity.Spawner.AddToSpawnQueue(itemSpawnInfo.ItemPrefab, entity.WorldPosition); + Entity.Spawner.AddToSpawnQueue(itemSpawnInfo.ItemPrefab, position); break; case ItemSpawnInfo.SpawnPositionType.ThisInventory: { @@ -761,10 +849,10 @@ namespace Barotrauma } } - ApplyProjSpecific(deltaTime, entity, targets, hull); + ApplyProjSpecific(deltaTime, entity, targets, hull, position); } - partial void ApplyProjSpecific(float deltaTime, Entity entity, List targets, Hull currentHull); + partial void ApplyProjSpecific(float deltaTime, Entity entity, List targets, Hull currentHull, Vector2 worldPosition); private void ApplyToProperty(ISerializableEntity target, SerializableProperty property, object value, float deltaTime) { diff --git a/Barotrauma/BarotraumaShared/Source/Utils/IPExtensions.cs b/Barotrauma/BarotraumaShared/Source/Utils/IPExtensions.cs new file mode 100644 index 000000000..954b6b127 --- /dev/null +++ b/Barotrauma/BarotraumaShared/Source/Utils/IPExtensions.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; + +namespace Barotrauma +{ + public static class IPExtensions + { + //workaround for .NET Framework 4.5 bug; presumably fixed in later versions + //see https://stackoverflow.com/questions/23608829/why-does-ipaddress-maptoipv4-throw-argumentoutofrangeexception + public static IPAddress MapToIPv4NoThrow(this IPAddress address) + { + byte[] addressBytes = address.GetAddressBytes(); + + return new IPAddress(addressBytes.Skip(addressBytes.Length - 4).ToArray()); + } + } +} diff --git a/Barotrauma/BarotraumaShared/Source/Utils/SaveUtil.cs b/Barotrauma/BarotraumaShared/Source/Utils/SaveUtil.cs index 52168a7e9..83fe7cb41 100644 --- a/Barotrauma/BarotraumaShared/Source/Utils/SaveUtil.cs +++ b/Barotrauma/BarotraumaShared/Source/Utils/SaveUtil.cs @@ -12,8 +12,8 @@ namespace Barotrauma { partial class SaveUtil { - private static string LegacySaveFolder = Path.Combine("Data", "Saves"); - private static string LegacyMultiplayerSaveFolder = Path.Combine(LegacySaveFolder, "Multiplayer"); + private static readonly string LegacySaveFolder = Path.Combine("Data", "Saves"); + private static readonly string LegacyMultiplayerSaveFolder = Path.Combine(LegacySaveFolder, "Multiplayer"); #if OSX //"/*user*/Library/Application Support/Daedalic Entertainment GmbH/" on Mac @@ -32,9 +32,10 @@ namespace Barotrauma "Barotrauma"); #endif - public static string MultiplayerSaveFolder = Path.Combine( - SaveFolder, - "Multiplayer"); + public static string MultiplayerSaveFolder = Path.Combine(SaveFolder, "Multiplayer"); + + public static readonly string SubmarineDownloadFolder = Path.Combine("Submarines", "Downloaded"); + public static readonly string CampaignDownloadFolder = Path.Combine("Data", "Saves", "Multiplayer"); public delegate void ProgressDelegate(string sMessage); @@ -46,7 +47,7 @@ namespace Barotrauma get { return Path.Combine(SaveFolder, "temp"); } #endif } - + public enum SaveType { Singleplayer, @@ -208,11 +209,11 @@ namespace Barotrauma if (Directory.Exists(legacyFolder)) { files.AddRange(Directory.GetFiles(legacyFolder, "*.save")); - } + } return files; } - + public static string CreateSavePath(SaveType saveType, string fileName = "Save_Default") { fileName = ToolBox.RemoveInvalidFileNameChars(fileName); @@ -229,7 +230,7 @@ namespace Barotrauma DebugConsole.Log("Save folder \"" + folder + "\" not found. Created new folder"); Directory.CreateDirectory(folder); } - + string extension = ".save"; string pathWithoutExtension = Path.Combine(folder, fileName); @@ -246,7 +247,7 @@ namespace Barotrauma return pathWithoutExtension + " " + i + extension; } - + public static void CompressStringToFile(string fileName, string value) { // A. @@ -296,8 +297,7 @@ namespace Barotrauma foreach (string sFilePath in sFiles) { string sRelativePath = sFilePath.Substring(iDirLen); - if (progress != null) - progress(sRelativePath); + progress?.Invoke(sRelativePath); CompressFile(sInDir, sRelativePath, str); } } @@ -328,7 +328,7 @@ namespace Barotrauma int iNameLen = BitConverter.ToInt32(bytes, 0); if (iNameLen > 255) { - throw new Exception("Failed to decompress \""+sDir+"\" (file name length > 255). The file may be corrupted."); + throw new Exception("Failed to decompress \"" + sDir + "\" (file name length > 255). The file may be corrupted."); } bytes = new byte[sizeof(char)]; @@ -340,8 +340,7 @@ namespace Barotrauma sb.Append(c); } string sFileName = sb.ToString(); - if (progress != null) - progress(sFileName); + progress?.Invoke(sFileName); //Decompress file content bytes = new byte[sizeof(int)]; @@ -438,9 +437,23 @@ namespace Barotrauma } } - public static void ClearFolder(string FolderName, string[] ignoredFileNames = null) + public static void CleanUnnecessarySaveFiles() { - DirectoryInfo dir = new DirectoryInfo(FolderName); + if (Directory.Exists(CampaignDownloadFolder)) + { + ClearFolder(CampaignDownloadFolder); + Directory.Delete(CampaignDownloadFolder); + } + if (Directory.Exists(TempPath)) + { + ClearFolder(TempPath); + Directory.Delete(TempPath); + } + } + + public static void ClearFolder(string folderName, string[] ignoredFileNames = null) + { + DirectoryInfo dir = new DirectoryInfo(folderName); foreach (FileInfo fi in dir.GetFiles()) { diff --git a/Barotrauma/BarotraumaShared/Source/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/Source/Utils/ToolBox.cs index 4e8c60ad1..138506c5f 100644 --- a/Barotrauma/BarotraumaShared/Source/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/Source/Utils/ToolBox.cs @@ -269,6 +269,7 @@ namespace Barotrauma public static string SecondsToReadableTime(float seconds) { + //TODO: localize time format int s = (int)(seconds % 60.0f); if (seconds < 60.0f) { diff --git a/Barotrauma/BarotraumaShared/Submarines/Bunyip.sub b/Barotrauma/BarotraumaShared/Submarines/Bunyip.sub index ca3cec2ce..4c8fa1c9e 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Bunyip.sub and b/Barotrauma/BarotraumaShared/Submarines/Bunyip.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Dugong.sub b/Barotrauma/BarotraumaShared/Submarines/Dugong.sub index 4cb7ac8de..bd3136da1 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 d3c33c61e..e70c13541 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Humpback.sub and b/Barotrauma/BarotraumaShared/Submarines/Humpback.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub b/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub index b593dd09c..bcac4074e 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub and b/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub b/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub index a5aa70838..7fb39e262 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub and b/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Orca.sub b/Barotrauma/BarotraumaShared/Submarines/Orca.sub index 8d87d3706..31fb35870 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Orca.sub and b/Barotrauma/BarotraumaShared/Submarines/Orca.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Remora.sub b/Barotrauma/BarotraumaShared/Submarines/Remora.sub index 31e005e3b..26b602dad 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Remora.sub and b/Barotrauma/BarotraumaShared/Submarines/Remora.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Selkie.sub b/Barotrauma/BarotraumaShared/Submarines/Selkie.sub index 0fe80c54e..4e349b07d 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Selkie.sub and b/Barotrauma/BarotraumaShared/Submarines/Selkie.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Typhon.sub b/Barotrauma/BarotraumaShared/Submarines/Typhon.sub index a1691f106..23dd98c29 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Typhon.sub and b/Barotrauma/BarotraumaShared/Submarines/Typhon.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Venture.sub b/Barotrauma/BarotraumaShared/Submarines/Venture.sub index 29820d9b5..b70b1237f 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Venture.sub and b/Barotrauma/BarotraumaShared/Submarines/Venture.sub differ diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index d056a9c77..1de7bcdb4 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,206 @@ +--------------------------------------------------------------------------------------------------------- +v0.9.5.1 +--------------------------------------------------------------------------------------------------------- + +- Fixed crashing when attempting to start a tutorial. +- Fixed Kastrull's drone not being docked to the sub by default. +- Fixed items occasionally launching through walls/floors when they're spawned or dropped. +- Nerfed Hammerhead Spawns. +- Fixed occasional disconnections when the Hammerhead Matriarch releases her spawns. +- Fixed Hammerhead Matriarch exploding twice when it attacks. +- Fixed bots sometimes getting stuck in an objective loop, causing them to repeatedly drop and pick up +diving suits or spam doors. +- Fixed bots being allowed to go outside without a diving suit. +- Fixed characters with a rectangular main collider being unable to use path finding. +- Fixed microphone volume scrollbar resetting to the maximum value when opening the settings menu. +- Fixed a bug that caused occasional crashes on Linux when pinging servers in the server list menu. + +--------------------------------------------------------------------------------------------------------- +v0.9.5.0 +--------------------------------------------------------------------------------------------------------- + +Misc additions and changes: +- A new submarine, Kastrull. +- 6 new traitor missions with much more varied objectives. +- More variants of all job outfits. +- 30 new character face sprites. +- Overhauled job assignment logic to make the job distribution a little more balanced. Now each client gets assigned one of the spawnpoints (and its associated job) according to their job preference, which means that if the sub for example has 2x as many engineer spawnpoints than medic spawnpoints, there tend to be 2x more engineers. When playing with a mod that adds new jobs to the game, spawnpoints that have no job associated with them are considered spawnpoints for the non-vanilla jobs. + - We would like to get feedback from players about this: does the job assignment seem fair, are people generally getting the jobs they want? +- New logic components: sin, cos, tan, asin, acos, atan, modulo, round, ceil, floor and factorial. +- Improved autopilot: much faster and less likely to get stuck. + - We would like to get feedback from players about this: Is there still incentive to steer manually? +- AI characters don't set stationary batteries to recharge unless ordered to do so. +- Items can be made damageable by projectiles, melee weapons and explosions by adding DamagedByProjectiles="true", DamagedByMeleeWeapons="true" or DamagedByExplosions="true" to the config. +- Nerfed welding fuel + diving mask combo. The tank depletes much faster and one tank is only enough to make the target fall unconscious, not to kill them. +- Added deconstruction recipes to all drugs. +- Added huskified face sprites to all characters. +- Question prompts in the debug console and dedicated server console (such as "enter the reason to ban XXX") can be cancelled by pressing Ctrl+D or Ctrl+Z (or Ctrl+C in the in-game debug console). +- Welding tools and plasma cutters explode when loaded with the wrong fuel. +- Removed sodium as a product of recycling fire extinguishers. +- Added some loose vents and panels to Orca and Typhon. +- Option to set a name for a character added by a mod without having to configure it in a separate text file. Use displayname="somemonster" in the character element to set the name. +- Balanced splash screen audio volumes. +- Alarm buzzers twitch when active to make it more clear where the sound is coming from. +- Scrollbars that work as on/off switches (e.g. power switches and autotemp sliders in item interfaces) can be toggled by clicking on them. +- Decreased the chance of selecting a traitor mission that's already been selected during the session. + +Multiplayer: +- Option to choose a "play style" for the servers to tell the players what kind of gameplay and rules they can expect on the server. +- Improved server browser: + - More filtering options + - Same version + - Show whitelisted + - Karma enabled + - Traitors enabled + - Friendly fire off + - Voice chat enabled + - Modded + - Option to filter by play style. + - Option to filter by game mode. + - Added the option to favorite servers. + - Added a list of recent servers. + - Added a friends list, you can keep track on which servers your friends are playing at as well as join them directly from the friends list. + - Miscellaneous layout improvements. +- Improved server lobby: + - Improved character customization interface: players now have full control over their appearance. + - Improved job preferences interface: players can now more easily switch around their class preferences, as well as pick which variation of an outfit they would like to wear. + - Mission selection is more flexible: mission types can be toggled separately. + - The campaign interface is displayed directly in the menu, not in a separate view. + - The submarine preview is displayed directly in the menu without having to open a separate window. + - The job preferences of other players are indicated in the player list. + - Voice chat icon now indicates the loudness of the audio being transmitted/received. + - Voice chat settings are indicated in the bottom-right corner of the chat box. + - The server log is integrated into the chat box. + - Added new server settings under anti-griefing tab which previously were only accessible through serversettings.xml. + - Allow friendly fire. + - Allow rewiring. + - Allow disguises. + - Miscellaneous layout changes. + +Human AI: +- Changed the logic for automatically unequipping the diving gear, extinguishers or items that are held in hands. +- Taught the bots to place diving suits, fuel rods and fire extinguishers into right places, if they can't unequip the items without dropping them AND if the current objective is not high priority. The system uses tags/identifiers and two new attributes: "preferredcontainers" in Items and "containablerestrictions" in ItemComponents so it can be adjusted both in the editor and in xml. +- Improved the code to prevent dead-locks between the AI states. +- Start looking new oxygen when the current oxygen tank is at 10% instead of starting when it's empty (which often lead the bot to suffocate while doing the objective). +- Combat: Don't ignore empty weapons when evaluating weapons for the first time (allows to try reloading a weapon before ignoring it as useless). +- Fight Intruders: Automatically switch weapon when the current weapon runs out of ammo. Find ammunition for the current weapon if no other decent weapon is found. Don't allow to go offensive while armed with poor weapons, like a wrench. +- Fix Leaks: Improved the operating of welding tools. +- Rescue: Major refactoring. Allow to heal self. Improve the priority calculation. The priority is now based on the severity of wounds. +- Taught the bots to look deeper into the inventories (recursive) when seeking items. +- Changed the decision making on whether or not a bot needs diving gear. +- The bots now avoid unsafe paths in the idle state or when doing low priority objectives. +- The bots don't follow targets outside of the submarine. +- The bots don't stay still when idling underwater.They swim in place instead. +- Adjusted the combat priorities for the weapons. +- Increased the minimum initiatives. +- Adjusted the target evaluation for the Fix Leaks objective. +- Improved the distance approximations for all AI objectives. +- Increased the base devotion (how much the current objective priority is boosted). +- Reduced the shooting distance when the bots are operating turrets. +- Adjusted the idle waiting, standing, and walking times. +- Fixed bots getting stuck if the next node was behind a door. +- Fixed bots getting stuck on ladders when the main collider bottom was slightly lower than the floor level, even though the bot should be able to take off from the ladders. +- Fixed bots getting stuck on ladders when they need a new target while climbing. +- Fixed bots ignoring/avoiding targets in hulls when there were dead enemies inside them. +- Fixed issues in switching empty oxygen tanks to new. +- Fixed bots unequipping and re-equipping diving gear while ordered to follow another character. +- Fixed bots not being able to handle things when they cannot find diving gear. Now they should first try to get the gear and if it fails, they should just target a safe hull. +- Fixed bots shooting friendly characters with turrets. +- Fixed bots always treating weapons as revolvers. +- Fixed the loading of smg magazines/fuel tanks etc (because they are different from revolver rounds). +- Fixed autonomic objectives overriding the user-defined settings (steering, batteries). +- Fixed bots being able to do things while hand-cuffed. +- Fixed a dead-end when an item is taken by another character after the target item was defined and before the objective is completed. +- Fixed item prioritization calculations. +- Fixed hull safety calculations always using the current visible hulls (which should only be taken into account when the hull is the current hull). +- Fixed bots abandoning the operate item objective if there was another character operating the item. +- Fixed bots speaking about success/failure to repair when another character fixes their target. +- Fixed bots reporting individual failures when doing a loop objective. Should fix bots spamming with the "Cannot reach the target" msgs. +- Fixed the current objective not being reassigned after sorting the objectives, leading to sorting the subobjectives of the old current objective, which would be fixed only after the next time the objectives were sorted (after 1 sec). +- Fixed the gap size being misinterpreted when calculating the priority for a fix leak objective. +- Fixed bots switching repair targets while repairing an item. +- Fixed bots in some situations facing the wrong direction. The bots should now always face the item they are operating. +- Fixed bots' ragdolls getting unstable due to constant flipping in some rare cases. +- Fixed bots not immediately targeting the other rooms when they reach the airlock (when entering the submarine). + +Monsters: +- New monster: Hammerhead Matriarch +- New monster: Hammerhead Spawn +- New diving suit sprite for husks. +- Characters now have a separate property that defines whether or not they can walk, meaning that characters can now enter the submarine even if they cannot walk. When there is no water, the character will fidget around. +Also fixes Fractal Guardians flying in non-flooded alien ruins. +- Monsters now remember the position of the last target even when idling. The memories fade, however. +- Monsters now flee briefly when being fired at with the turrets. Fixes swarming characters being easy to exploit. +- Don't allow the monsters to retaliate unless they are in the same submarine as the attacker. Removed the CanRetaliate parameter on character AI that was added in the previous update. +- Refactored ConditionalSprite: ConditionalSprite now takes Sprite or DeformableSprite as child elements. +- StatusEffects can now be used to spawn characters. +- Monster missions now support multiple monsters of different species. +- Minor adjustments on eating. Fixes Bonethresher's eating animation. +- Improved the avoid steering logic (used when the character is not using pathfiding, i.e. outside the submarine). +- Charybdis: Increase the damage against structures for all attacks. +- Adjusted Mudraptor, Bonethresher and Tigerthersher behavior. Threshers now hunt in packs. +- Fixed incorrect flipping and mirroring. Disable mirroring on characters that don't need it. +- Fixed Mudraptors preferring doors over humans. +- Fixed targeting at wall center position instead of the section position. Fixes hammerheads and bonethreshers targeting destroyed wall sections. +- Fixed the damage modifiers on hammerheads not working properly. +- Fixed a crash when trying to create a swarm with characters that have no swarming behavior defined. +- Fixed monsters not targeting the closest limb when they should. +- Fixed ai targets not being updated when the character is dead, causing the characters to be too perceivable/not perceivable enough by the monsters. +- Fixed limbs' lightsources not being visible inside the submarine. +- Fixed immobilized characters (e.g. stunned, monsters on dry land) occasionally "hanging" mid-air. +- Fixed draw order issues due to normal sprites and deformable sprites being used on the same characters. + +Character editor: +- Added "TailTorqueBoost" parameter that can be used to prevent long and fast-moving characters, like the Tigerthresher, to get tangled around themselves. +- "TailTorque" no longer affects the swimming sine amplitude (how strong the tail moves). +- Sprite orientation no longer affects the damage modifiers. +- Exposed the ragdoll main limb. +- Exposed the damage emitters. +- Exposed the damage sound of damage modifiers. +- The radial widget angles are now rounded to the closest 10. +- Fixed characters being able to collide with the submarine, which often caused large monsters to get stunned and impact sounds to be played when they're spawned. +- Fixed characters moving when they shouldn't. + +Bugfixes: +- Rewrote power distribution logic to resolve several issues and oddities in the way power was distributed across electrical grids, particularly when relays were used. The new system is also less performance intensive, so submarines that had performance issues due to large and complex power grids should now run better. +- Fixed inability for mods to override jobs. +- Fixed pumps calculating targetlevel incorrectly when receiving a "set_targetlevel" signal, causing neutral ballast level value in the sub editor to be off. +- Fixed clients being able to join servers when they're using content packages the server doesn't have, which usually causes the client to get disconnected when a round starts. +- Fixed clients being able to join servers after enabling a required content package in the settings, even though they need to restart first (leading to errors when a round starts). +- Fixed "attempted to access a potentially removed character" console errors if a character dies while the client is waiting to apply the character's remote inventory state. +- Fixed broken waypoints in Typhon that prevented bots from opening some of the doors. +- Fixed characters being very hard or impossible to hit with melee weapons when they're lying on the floor. +- Fixed characters placing their feet above a platform when not actually standing on the platform (noticeable in rooms where there's a platform on top of the "actual" floor). +- Fixed husk eggs not causing a husk infection when injected. +- Fixed depth sorting not working on damageable structures. +- Fixed characters stepping on nothing if their feet are below the lowest step of a ladder. +- Fixed door components being selectable when the door is broken, causing characters to drop from ladders if they interact with a door while climbing. +- Alien motion sensors don't react to dead bodies to prevent bodies from blocking the way through the ruins by constantly activating coils. +- Fixes to waypoints and outdated structure sourcerects in Bunyip, Selkie & Venture. +- Put the shotgun shells in Dugong's, Kastrull's and Humpback's armory inside the shotgun. +- Fixed clients retaining their original character when taking control of another one in the multiplayer campaign, but keeping the items of the new character. +- Fixed sourcerects being moved when going through the sprite lists with arrow keys in the sprite editor. +- Fixed servers occasionally appearing in the server list twice. +- Improved color input layout in EntityEditors to prevent overlaps. +- Fixed flares not depleting when in inventory. +- Fixed propeller position becoming unmirrored when saving and reloading a mirrored engine. +- Fixed detection area offset becoming unmirrored when saving and reloading a mirrored motion sensor. +- Fixed a couple of doors and hatches in vanilla subs being repairable with welding tools instead of wrenches. +- Fixed ruins occasionally having rooms too narrow to pass through. +- Fixed door gaps occasionally getting misplaced when placing them in ruins, preventing water from flowing through the room. +- Fixed "picked/selected required" fields disappearing from the item editing panel in the submarine editor if left blank. +- Fixed being able to hit through walls with melee weapons. +- Fixed framerate issues when using passive sonar near large amounts of sound emitters (right outside a submarine for example). +- Fixed empty exploding ammo boxes exploding when deconstructed. +- Fixed old chat messages overlapping in the server lobby after the scrollbar of the chatbox becomes active. +- Fixed textbox selection not being cleared when pressing Ctrl+Z or Ctrl+R, causing a crash if the selection is outside the bounds of the current text and the player attempts to remove text from the box. +- Don't allow reporting things while inside ruins. +- Fixed crashing if a client has written something in the chatbox, gets disconnected and sends the message. +- Fixed servers showing up in the server lobby after the server has been shut down. +- Fixed traitor missions not ending when the traitor dies and the traitor retaining their old mission info in the info menu. +- Fixed dead characters still appearing in the crew list when a client joins mid-round. +- Fixes to decorative sprites: Rotation property is in degrees, fixed offset anim type being used as the rotation anim. + --------------------------------------------------------------------------------------------------------- v0.9.4.0 --------------------------------------------------------------------------------------------------------- @@ -11,7 +214,7 @@ Monster additions and changes: - Medium sized monsters can now more easily trigger impact effects when ramming the submarine. - Characters are now allowed to move faster than previously (9 -> 15). - Changed the mouth position calculations and redefined the values for all monsters that have the eating behavior. -- Removed Moloch Baby (Which was just a test. Molochs will be revisited in the next update). +- Removed Moloch Baby (Which was just a test. Molochs will be revisited later). - Better bend deformations on limbs. - Better feedback about hit impacts on limbs when the character takes damage. - AI monsters should hit moving targets more easily. diff --git a/Barotrauma/BarotraumaShared/config.xml b/Barotrauma/BarotraumaShared/config.xml index 7821a1e40..38238f79c 100644 --- a/Barotrauma/BarotraumaShared/config.xml +++ b/Barotrauma/BarotraumaShared/config.xml @@ -41,17 +41,9 @@ Ragdoll="Space" Health="H" Grab="G" + Shoot="0" + Deselect="1" SelectPreviousCharacter="Z" SelectNextCharacter="X" /> - - - - - - - - - - diff --git a/Libraries/Facepunch.Steamworks/Client/Friends.cs b/Libraries/Facepunch.Steamworks/Client/Friends.cs index f49a950b5..41c31fed1 100644 --- a/Libraries/Facepunch.Steamworks/Client/Friends.cs +++ b/Libraries/Facepunch.Steamworks/Client/Friends.cs @@ -36,10 +36,13 @@ namespace Facepunch.Steamworks internal Client client; private byte[] buffer = new byte[1024 * 128]; + public Dictionary OnRichPresenceUpdateCallbacks; + internal Friends( Client c ) { client = c; + client.RegisterCallback(OnRichPresenceUpdate); client.RegisterCallback( OnAvatarImageLoaded ); client.RegisterCallback( OnPersonaStateChange ); client.RegisterCallback( OnGameJoinRequested ); @@ -149,6 +152,33 @@ namespace Facepunch.Steamworks } } + public void SetRichPresenceUpdateCallback(ulong steamId, Action callback) + { + if (callback != null) + { + if (OnRichPresenceUpdateCallbacks == null) + { + OnRichPresenceUpdateCallbacks = new Dictionary(); + } + if (!OnRichPresenceUpdateCallbacks.ContainsKey(steamId)) + { + OnRichPresenceUpdateCallbacks.Add(steamId, callback); + } + else + { + OnRichPresenceUpdateCallbacks[steamId] = callback; + } + } + else + { + if (OnRichPresenceUpdateCallbacks == null) { return; } + if (OnRichPresenceUpdateCallbacks.ContainsKey(steamId)) + { + OnRichPresenceUpdateCallbacks.Remove(steamId); + } + } + } + /// /// Returns only friends /// @@ -392,5 +422,14 @@ namespace Facepunch.Steamworks LoadAvatarForSteamId( data.SteamID ); } + private void OnRichPresenceUpdate( FriendRichPresenceUpdate_t data ) + { + if (OnRichPresenceUpdateCallbacks == null) { return; } + if (OnRichPresenceUpdateCallbacks.ContainsKey(data.SteamIDFriend)) + { + OnRichPresenceUpdateCallbacks[data.SteamIDFriend]?.Invoke(); + } + } + } } diff --git a/Libraries/Facepunch.Steamworks/Client/LobbyList.cs b/Libraries/Facepunch.Steamworks/Client/LobbyList.cs index 9614057ec..c161bf3b3 100644 --- a/Libraries/Facepunch.Steamworks/Client/LobbyList.cs +++ b/Libraries/Facepunch.Steamworks/Client/LobbyList.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Text; using SteamNative; + +//TODO: this entire file is the main reason why we need to update this library namespace Facepunch.Steamworks { public partial class LobbyList : IDisposable @@ -12,6 +14,8 @@ namespace Facepunch.Steamworks //The list of retrieved lobbies public List Lobbies { get; private set; } + public Dictionary> ManualLobbyDataCallbacks { get; private set; } + //True when all the possible lobbies have had their data updated //if the number of lobbies is now equal to the initial request number, we've found all lobbies public bool Finished { get; private set; } @@ -21,9 +25,13 @@ namespace Facepunch.Steamworks internal LobbyList(Client client) { + client.RegisterCallback(OnLobbyDataUpdated); + this.client = client; Lobbies = new List(); requests = new List(); + + ManualLobbyDataCallbacks = new Dictionary>(); } /// @@ -77,7 +85,6 @@ namespace Facepunch.Steamworks } - bool registeredLobbyDataUpdated = false; void OnLobbyList(LobbyMatchList_t callback, bool error) { if (error) return; @@ -107,11 +114,6 @@ namespace Facepunch.Steamworks { //else we need to get the info for the missing lobby client.native.matchmaking.RequestLobbyData(lobby); - if (!registeredLobbyDataUpdated) - { - client.RegisterCallback(OnLobbyDataUpdated); - registeredLobbyDataUpdated = true; - } } } @@ -135,24 +137,72 @@ namespace Facepunch.Steamworks { if (callback.Success == 1) //1 if success, 0 if failure { + if (ManualLobbyDataCallbacks.ContainsKey(callback.SteamIDLobby)) + { + ManualLobbyDataCallbacks[callback.SteamIDLobby]?.Invoke(Lobby.FromSteam(client, callback.SteamIDLobby)); + } + //find the lobby that has been updated Lobby lobby = Lobbies.Find(x => x != null && x.LobbyID == callback.SteamIDLobby); //if this lobby isn't yet in the list of lobbies, we know that we should add it if (lobby == null) { - lobby = Lobby.FromSteam(client, callback.SteamIDLobby); - Lobbies.Add(lobby); - checkFinished(); + if (requests.Contains(callback.SteamIDLobby)) + { + lobby = Lobby.FromSteam(client, callback.SteamIDLobby); + Lobbies.Add(lobby); + checkFinished(); + } } //otherwise lobby data in general was updated and you should listen to see what changed - if (OnLobbiesUpdated != null) { OnLobbiesUpdated(); } + if (requests.Contains(callback.SteamIDLobby)) + { + OnLobbiesUpdated?.Invoke(); + } } } public Action OnLobbiesUpdated; + public Lobby GetLobbyFromID(ulong lobbyId) + { + return Lobby.FromSteam(client, lobbyId); + } + + public void RequestLobbyData(ulong lobby) + { + client.native.matchmaking.RequestLobbyData(lobby); + } + + public void SetManualLobbyDataCallback(ulong steamId, Action callback) + { + if (callback != null) + { + if (ManualLobbyDataCallbacks == null) + { + ManualLobbyDataCallbacks = new Dictionary>(); + } + if (!ManualLobbyDataCallbacks.ContainsKey(steamId)) + { + ManualLobbyDataCallbacks.Add(steamId, callback); + } + else + { + ManualLobbyDataCallbacks[steamId] = callback; + } + } + else + { + if (ManualLobbyDataCallbacks == null) { return; } + if (ManualLobbyDataCallbacks.ContainsKey(steamId)) + { + ManualLobbyDataCallbacks.Remove(steamId); + } + } + } + public void Dispose() { client = null; diff --git a/Libraries/Facepunch.Steamworks/Client/ServerList.cs b/Libraries/Facepunch.Steamworks/Client/ServerList.cs index 407d98f40..6fbb6ef80 100644 --- a/Libraries/Facepunch.Steamworks/Client/ServerList.cs +++ b/Libraries/Facepunch.Steamworks/Client/ServerList.cs @@ -1,12 +1,349 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Runtime.InteropServices; using System.Text; using SteamNative; namespace Facepunch.Steamworks { + //ISteamMatchmakingRulesResponse & ISteamMatchmakingPlayersResponse taken from: + // https://github.com/rlabrecque/Steamworks.NET/blob/master/Plugins/Steamworks.NET/ISteamMatchmakingResponses.cs + + /** + + The MIT License (MIT) + + Copyright (c) 2013-2019 Riley Labrecque + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + + **/ + public class ISteamMatchmakingPingResponse + { + // Server has responded successfully and has updated data + public delegate void ServerResponded(ServerList.Server server); + + // Server failed to respond to the ping request + public delegate void ServerFailedToRespond(); + + private VTable m_VTable; + private IntPtr m_pVTable; + private GCHandle m_pGCHandle; + private ServerResponded m_ServerResponded; + private ServerFailedToRespond m_ServerFailedToRespond; + private Client client; + + public ISteamMatchmakingPingResponse(Client c, ServerResponded onServerResponded, ServerFailedToRespond onServerFailedToRespond) + { + if (onServerResponded == null || onServerFailedToRespond == null) + { + throw new ArgumentNullException(); + } + client = c; + m_ServerResponded = onServerResponded; + m_ServerFailedToRespond = onServerFailedToRespond; + + m_VTable = new VTable() + { + m_VTServerResponded = InternalOnServerResponded, + m_VTServerFailedToRespond = InternalOnServerFailedToRespond, + }; + m_pVTable = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(VTable))); + Marshal.StructureToPtr(m_VTable, m_pVTable, false); + + m_pGCHandle = GCHandle.Alloc(m_pVTable, GCHandleType.Pinned); + } + + ~ISteamMatchmakingPingResponse() + { + if (m_pVTable != IntPtr.Zero) + { + Marshal.FreeHGlobal(m_pVTable); + } + + if (m_pGCHandle.IsAllocated) + { + m_pGCHandle.Free(); + } + } + + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] + private delegate void InternalServerResponded(IntPtr thisptr, gameserveritem_t server); + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] + private delegate void InternalServerFailedToRespond(IntPtr thisptr); + private void InternalOnServerResponded(IntPtr thisptr, gameserveritem_t serverItem) + { + m_ServerResponded(ServerList.Server.FromSteam(client, serverItem)); + } + private void InternalOnServerFailedToRespond(IntPtr thisptr) + { + m_ServerFailedToRespond(); + } + + [StructLayout(LayoutKind.Sequential)] + private class VTable + { + [NonSerialized] + [MarshalAs(UnmanagedType.FunctionPtr)] + public InternalServerResponded m_VTServerResponded; + + [NonSerialized] + [MarshalAs(UnmanagedType.FunctionPtr)] + public InternalServerFailedToRespond m_VTServerFailedToRespond; + } + + public static explicit operator System.IntPtr(ISteamMatchmakingPingResponse that) + { + return that.m_pGCHandle.AddrOfPinnedObject(); + } + }; + + public class ISteamMatchmakingRulesResponse + { + // Got data on a rule on the server -- you'll get one of these per rule defined on + // the server you are querying + public delegate void RulesResponded(string pchRule, string pchValue); + + // The server failed to respond to the request for rule details + public delegate void RulesFailedToRespond(); + + // The server has finished responding to the rule details request + // (ie, you won't get anymore RulesResponded callbacks) + public delegate void RulesRefreshComplete(); + + private VTable m_VTable; + private IntPtr m_pVTable; + private GCHandle m_pGCHandle; + private RulesResponded m_RulesResponded; + private RulesFailedToRespond m_RulesFailedToRespond; + private RulesRefreshComplete m_RulesRefreshComplete; + + public ISteamMatchmakingRulesResponse(RulesResponded onRulesResponded, RulesFailedToRespond onRulesFailedToRespond, RulesRefreshComplete onRulesRefreshComplete) + { + if (onRulesResponded == null || onRulesFailedToRespond == null || onRulesRefreshComplete == null) + { + throw new ArgumentNullException(); + } + m_RulesResponded = onRulesResponded; + m_RulesFailedToRespond = onRulesFailedToRespond; + m_RulesRefreshComplete = onRulesRefreshComplete; + + m_VTable = new VTable() + { + m_VTRulesResponded = InternalOnRulesResponded, + m_VTRulesFailedToRespond = InternalOnRulesFailedToRespond, + m_VTRulesRefreshComplete = InternalOnRulesRefreshComplete + }; + m_pVTable = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(VTable))); + Marshal.StructureToPtr(m_VTable, m_pVTable, false); + + m_pGCHandle = GCHandle.Alloc(m_pVTable, GCHandleType.Pinned); + } + + ~ISteamMatchmakingRulesResponse() + { + if (m_pVTable != IntPtr.Zero) + { + Marshal.FreeHGlobal(m_pVTable); + } + + if (m_pGCHandle.IsAllocated) + { + m_pGCHandle.Free(); + } + } + + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] + public delegate void InternalRulesResponded(IntPtr thisptr, IntPtr pchRule, IntPtr pchValue); + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] + public delegate void InternalRulesFailedToRespond(IntPtr thisptr); + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] + public delegate void InternalRulesRefreshComplete(IntPtr thisptr); + private void InternalOnRulesResponded(IntPtr thisptr, IntPtr pchRule, IntPtr pchValue) + { + List bytes = new List(); + IntPtr seekPointer = pchRule; + byte b = Marshal.ReadByte(seekPointer); + while (b != 0) + { + bytes.Add(b); + seekPointer = (IntPtr)(seekPointer.ToInt64() + sizeof(byte)); + b = Marshal.ReadByte(seekPointer); + } + + string pchRuleDecoded = Encoding.UTF8.GetString(bytes.ToArray()); + + bytes.Clear(); + seekPointer = pchValue; + b = Marshal.ReadByte(seekPointer); + while (b != 0) + { + bytes.Add(b); + seekPointer = (IntPtr)(seekPointer.ToInt64() + sizeof(byte)); + b = Marshal.ReadByte(seekPointer); + } + + string pchValueDecoded = Encoding.UTF8.GetString(bytes.ToArray()); + + m_RulesResponded(pchRuleDecoded, pchValueDecoded); + } + private void InternalOnRulesFailedToRespond(IntPtr thisptr) + { + m_RulesFailedToRespond(); + } + private void InternalOnRulesRefreshComplete(IntPtr thisptr) + { + m_RulesRefreshComplete(); + } + + [StructLayout(LayoutKind.Sequential)] + private class VTable + { + [NonSerialized] + [MarshalAs(UnmanagedType.FunctionPtr)] + public InternalRulesResponded m_VTRulesResponded; + + [NonSerialized] + [MarshalAs(UnmanagedType.FunctionPtr)] + public InternalRulesFailedToRespond m_VTRulesFailedToRespond; + + [NonSerialized] + [MarshalAs(UnmanagedType.FunctionPtr)] + public InternalRulesRefreshComplete m_VTRulesRefreshComplete; + } + + public static explicit operator System.IntPtr(ISteamMatchmakingRulesResponse that) + { + return that.m_pGCHandle.AddrOfPinnedObject(); + } + }; + + public class ISteamMatchmakingPlayersResponse + { + // Got data on a new player on the server -- you'll get this callback once per player + // on the server which you have requested player data on. + public delegate void AddPlayerToList(string pchName, int nScore, float flTimePlayed); + + // The server failed to respond to the request for player details + public delegate void PlayersFailedToRespond(); + + // The server has finished responding to the player details request + // (ie, you won't get anymore AddPlayerToList callbacks) + public delegate void PlayersRefreshComplete(); + + private VTable m_VTable; + private IntPtr m_pVTable; + private GCHandle m_pGCHandle; + private AddPlayerToList m_AddPlayerToList; + private PlayersFailedToRespond m_PlayersFailedToRespond; + private PlayersRefreshComplete m_PlayersRefreshComplete; + + public ISteamMatchmakingPlayersResponse(AddPlayerToList onAddPlayerToList, PlayersFailedToRespond onPlayersFailedToRespond, PlayersRefreshComplete onPlayersRefreshComplete) + { + if (onAddPlayerToList == null || onPlayersFailedToRespond == null || onPlayersRefreshComplete == null) + { + throw new ArgumentNullException(); + } + m_AddPlayerToList = onAddPlayerToList; + m_PlayersFailedToRespond = onPlayersFailedToRespond; + m_PlayersRefreshComplete = onPlayersRefreshComplete; + + m_VTable = new VTable() + { + m_VTAddPlayerToList = InternalOnAddPlayerToList, + m_VTPlayersFailedToRespond = InternalOnPlayersFailedToRespond, + m_VTPlayersRefreshComplete = InternalOnPlayersRefreshComplete + }; + m_pVTable = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(VTable))); + Marshal.StructureToPtr(m_VTable, m_pVTable, false); + + m_pGCHandle = GCHandle.Alloc(m_pVTable, GCHandleType.Pinned); + } + + ~ISteamMatchmakingPlayersResponse() + { + if (m_pVTable != IntPtr.Zero) + { + Marshal.FreeHGlobal(m_pVTable); + } + + if (m_pGCHandle.IsAllocated) + { + m_pGCHandle.Free(); + } + } + + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] + public delegate void InternalAddPlayerToList(IntPtr thisptr, IntPtr pchName, int nScore, float flTimePlayed); + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] + public delegate void InternalPlayersFailedToRespond(IntPtr thisptr); + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] + public delegate void InternalPlayersRefreshComplete(IntPtr thisptr); + private void InternalOnAddPlayerToList(IntPtr thisptr, IntPtr pchName, int nScore, float flTimePlayed) + { + List bytes = new List(); + IntPtr seekPointer = pchName; + byte b = Marshal.ReadByte(seekPointer); + while (b != 0) + { + bytes.Add(b); + seekPointer = (IntPtr)(seekPointer.ToInt64() + sizeof(byte)); + b = Marshal.ReadByte(seekPointer); + } + + string pchNameDecoded = Encoding.UTF8.GetString(bytes.ToArray()); + + m_AddPlayerToList(pchNameDecoded, nScore, flTimePlayed); + } + private void InternalOnPlayersFailedToRespond(IntPtr thisptr) + { + m_PlayersFailedToRespond(); + } + private void InternalOnPlayersRefreshComplete(IntPtr thisptr) + { + m_PlayersRefreshComplete(); + } + + [StructLayout(LayoutKind.Sequential)] + private class VTable + { + [NonSerialized] + [MarshalAs(UnmanagedType.FunctionPtr)] + public InternalAddPlayerToList m_VTAddPlayerToList; + + [NonSerialized] + [MarshalAs(UnmanagedType.FunctionPtr)] + public InternalPlayersFailedToRespond m_VTPlayersFailedToRespond; + + [NonSerialized] + [MarshalAs(UnmanagedType.FunctionPtr)] + public InternalPlayersRefreshComplete m_VTPlayersRefreshComplete; + } + + public static explicit operator System.IntPtr(ISteamMatchmakingPlayersResponse that) + { + return that.m_pGCHandle.AddrOfPinnedObject(); + } + }; + public partial class ServerList : IDisposable { internal Client client; @@ -124,7 +461,20 @@ namespace Facepunch.Steamworks public string value; } + public void CancelHQuery(int query) + { + client.native.servers.CancelServerQuery((HServerQuery)query); + } + public int HQueryPing(ISteamMatchmakingPingResponse response, IPAddress ip, int port) + { + return client.native.servers.PingServer(ip.IpToInt32(), (ushort)port, (IntPtr)response).Value; + } + + public int HQueryServerRules(ISteamMatchmakingRulesResponse response, IPAddress ip, int queryPort) + { + return client.native.servers.ServerRules(ip.IpToInt32(), (ushort)queryPort, (IntPtr)response).Value; + } public Request Internet( Filter filter = null ) { diff --git a/Libraries/Facepunch.Steamworks/Interfaces/Workshop.Query.cs b/Libraries/Facepunch.Steamworks/Interfaces/Workshop.Query.cs index d02f351b6..2cbba5499 100644 --- a/Libraries/Facepunch.Steamworks/Interfaces/Workshop.Query.cs +++ b/Libraries/Facepunch.Steamworks/Interfaces/Workshop.Query.cs @@ -85,21 +85,30 @@ namespace Facepunch.Steamworks unsafe void RunInternal() { + string queryType = ""; if ( FileId.Count != 0 ) { var fileArray = FileId.Select( x => (SteamNative.PublishedFileId_t)x ).ToArray(); _resultsRemain = fileArray.Length; Handle = workshop.ugc.CreateQueryUGCDetailsRequest( fileArray ); + queryType = "DetailsRequest"; } else if ( UserId.HasValue ) { uint accountId = (uint)( UserId.Value & 0xFFFFFFFFul ); Handle = workshop.ugc.CreateQueryUserUGCRequest( accountId, (SteamNative.UserUGCList)( int)UserQueryType, (SteamNative.UGCMatchingUGCType)( int)QueryType, SteamNative.UserUGCListSortOrder.LastUpdatedDesc, UploaderAppId, AppId, (uint)_resultPage + 1 ); + queryType = "UserRequest"; } else { Handle = workshop.ugc.CreateQueryAllUGCRequest( (SteamNative.UGCQuery)(int)Order, (SteamNative.UGCMatchingUGCType)(int)QueryType, UploaderAppId, AppId, (uint)_resultPage + 1 ); + queryType = "AllRequest"; + } + + if (Handle == 0xfffffffffffffffful) + { + throw new Exception("Steam UGC "+queryType+" Query Handle invalid!"); } if ( !string.IsNullOrEmpty( SearchText ) ) @@ -246,7 +255,7 @@ namespace Facepunch.Steamworks public void Dispose() { - // ReleaseQueryUGCRequest + workshop.ugc.ReleaseQueryUGCRequest(Handle); } } diff --git a/Libraries/MonoGame.Framework/DesktopGL/MonoGame.Framework.dll b/Libraries/MonoGame.Framework/DesktopGL/MonoGame.Framework.dll index 39c64ff3a..c5e6f5164 100644 Binary files a/Libraries/MonoGame.Framework/DesktopGL/MonoGame.Framework.dll and b/Libraries/MonoGame.Framework/DesktopGL/MonoGame.Framework.dll differ diff --git a/Libraries/MonoGame.Framework/DesktopGL/MonoGame.Framework.pdb b/Libraries/MonoGame.Framework/DesktopGL/MonoGame.Framework.pdb new file mode 100644 index 000000000..7b3b164e8 Binary files /dev/null and b/Libraries/MonoGame.Framework/DesktopGL/MonoGame.Framework.pdb differ diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/GraphicsDevice.DirectX.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/GraphicsDevice.DirectX.cs index 5625ce156..c5fef7f3d 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/GraphicsDevice.DirectX.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/GraphicsDevice.DirectX.cs @@ -520,8 +520,9 @@ namespace Microsoft.Xna.Framework.Graphics if (_d3dContext != null) _d3dContext.Dispose(); - // Windows requires BGRA support out of DX. - var creationFlags = SharpDX.Direct3D11.DeviceCreationFlags.BgraSupport; + // Windows requires BGRA support out of DX. (...what) + // Barotrauma doesn't tho + var creationFlags = SharpDX.Direct3D11.DeviceCreationFlags.None;//.BgraSupport; if (GraphicsAdapter.UseDebugLayers) { @@ -545,14 +546,14 @@ namespace Microsoft.Xna.Framework.Graphics { featureLevels = new[] { - // For the Reach profile, first try use the highest supported 9_X feature level - FeatureLevel.Level_9_3, - FeatureLevel.Level_9_2, - FeatureLevel.Level_9_1, - // If level 9 is not supported, then just use the highest supported level + // Try using the highest supported level FeatureLevel.Level_11_0, FeatureLevel.Level_10_1, FeatureLevel.Level_10_0, + // Then try using the highest supported 9_X feature level + FeatureLevel.Level_9_3, + FeatureLevel.Level_9_2, + FeatureLevel.Level_9_1, }; } @@ -583,7 +584,7 @@ namespace Microsoft.Xna.Framework.Graphics _d3dDevice = defaultDevice.QueryInterface(); } - // Get Direct3D 11.1 context + // Get Direct3D 11 context _d3dContext = _d3dDevice.ImmediateContext.QueryInterface(); // Create a new instance of GraphicsDebug because we support it on Windows platforms. @@ -600,24 +601,6 @@ namespace Microsoft.Xna.Framework.Graphics _swapChain.SetFullscreenState(false, null); } - internal void ResizeTargets() - { - var format = SharpDXHelper.ToFormat(PresentationParameters.BackBufferFormat); - var descr = new ModeDescription - { - Format = format, -#if WINRT - Scaling = DisplayModeScaling.Stretched, -#else - Scaling = DisplayModeScaling.Unspecified, -#endif - Width = PresentationParameters.BackBufferWidth, - Height = PresentationParameters.BackBufferHeight, - }; - - _swapChain.ResizeTarget(ref descr); - } - internal void GetModeSwitchedSize(out int width, out int height) { Output output = null; @@ -717,76 +700,59 @@ namespace Microsoft.Xna.Framework.Graphics format, PresentationParameters.MultiSampleCount); - // If the swap chain already exists... update it. - if (false && _swapChain != null - // check if multisampling hasn't changed - && _swapChain.Description.SampleDescription.Count == multisampleDesc.Count - && _swapChain.Description.SampleDescription.Quality == multisampleDesc.Quality) + // Create a new swap chain. + var wasFullScreen = false; + // Dispose of old swap chain if exists + if (_swapChain != null) { - _swapChain.ResizeBuffers(2, - PresentationParameters.BackBufferWidth, - PresentationParameters.BackBufferHeight, - format, - SwapChainFlags.AllowModeSwitch); + wasFullScreen = _swapChain.IsFullScreen; + // Before releasing a swap chain, first switch to windowed mode + _swapChain.SetFullscreenState(false, null); + _swapChain.Dispose(); } - // Otherwise, create a new swap chain. - else + // SwapChain description + var desc = new SharpDX.DXGI.SwapChainDescription() { - var wasFullScreen = false; - // Dispose of old swap chain if exists - if (_swapChain != null) + ModeDescription = { - wasFullScreen = _swapChain.IsFullScreen; - // Before releasing a swap chain, first switch to windowed mode - _swapChain.SetFullscreenState(false, null); - _swapChain.Dispose(); - } - - // SwapChain description - var desc = new SharpDX.DXGI.SwapChainDescription() - { - ModeDescription = - { - Format = format, + Format = format, #if WINDOWS_UAP - Scaling = DisplayModeScaling.Stretched, + Scaling = DisplayModeScaling.Stretched, #else - Scaling = DisplayModeScaling.Unspecified, + Scaling = DisplayModeScaling.Unspecified, #endif - Width = PresentationParameters.BackBufferWidth, - Height = PresentationParameters.BackBufferHeight, - }, + Width = PresentationParameters.BackBufferWidth, + Height = PresentationParameters.BackBufferHeight, + }, - OutputHandle = PresentationParameters.DeviceWindowHandle, - SampleDescription = multisampleDesc, - Usage = SharpDX.DXGI.Usage.RenderTargetOutput, - BufferCount = 2, - SwapEffect = SharpDXHelper.ToSwapEffect(PresentationParameters.PresentationInterval), - IsWindowed = true, - Flags = SwapChainFlags.AllowModeSwitch - }; + OutputHandle = PresentationParameters.DeviceWindowHandle, + SampleDescription = multisampleDesc, + Usage = SharpDX.DXGI.Usage.RenderTargetOutput, + BufferCount = 2, + SwapEffect = SharpDXHelper.ToSwapEffect(PresentationParameters.PresentationInterval), + IsWindowed = true + }; - // Once the desired swap chain description is configured, it must be created on the same adapter as our D3D Device + // Once the desired swap chain description is configured, it must be created on the same adapter as our D3D Device - // First, retrieve the underlying DXGI Device from the D3D Device. - // Creates the swap chain - using (var dxgiDevice = _d3dDevice.QueryInterface()) - using (var dxgiAdapter = dxgiDevice.Adapter) - using (var dxgiFactory = dxgiAdapter.GetParent()) - { - _swapChain = new SwapChain(dxgiFactory, dxgiDevice, desc); - RefreshAdapter(); - dxgiFactory.MakeWindowAssociation(PresentationParameters.DeviceWindowHandle, WindowAssociationFlags.IgnoreAll); - // To reduce latency, ensure that DXGI does not queue more than one frame at a time. - // Docs: https://msdn.microsoft.com/en-us/library/windows/desktop/ff471334(v=vs.85).aspx - dxgiDevice.MaximumFrameLatency = 1; - } - // Preserve full screen state, after swap chain is re-created - if (PresentationParameters.HardwareModeSwitch - && wasFullScreen) - SetHardwareFullscreen(); + // First, retrieve the underlying DXGI Device from the D3D Device. + // Creates the swap chain + using (var dxgiDevice = _d3dDevice.QueryInterface()) + using (var dxgiAdapter = dxgiDevice.Adapter) + using (var dxgiFactory = dxgiAdapter.GetParent()) + { + _swapChain = new SwapChain(dxgiFactory, dxgiDevice, desc); + RefreshAdapter(); + dxgiFactory.MakeWindowAssociation(PresentationParameters.DeviceWindowHandle, WindowAssociationFlags.IgnoreAll); + // To reduce latency, ensure that DXGI does not queue more than one frame at a time. + // Docs: https://msdn.microsoft.com/en-us/library/windows/desktop/ff471334(v=vs.85).aspx + dxgiDevice.MaximumFrameLatency = 1; } + // Preserve full screen state, after swap chain is re-created + if (PresentationParameters.HardwareModeSwitch + && wasFullScreen) + SetHardwareFullscreen(); // Obtain the backbuffer for this window which will be the final 3D rendertarget. Point targetSize; diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/Texture2D.DirectX.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/Texture2D.DirectX.cs index f1f537056..2cce0c726 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/Texture2D.DirectX.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/Texture2D.DirectX.cs @@ -72,7 +72,10 @@ namespace Microsoft.Xna.Framework.Graphics var subresourceIndex = CalculateSubresourceIndex(0, level); var d3dContext = GraphicsDevice._d3dContext; lock (d3dContext) + { d3dContext.UpdateSubresource(GetTexture(), subresourceIndex, region, dataPtr, GetPitch(w), 0); + d3dContext.GenerateMips(GetShaderResourceView()); + } } finally { @@ -390,6 +393,12 @@ namespace Microsoft.Xna.Framework.Graphics if (_shared) desc.OptionFlags |= ResourceOptionFlags.Shared; + if (_mipmap) + { + desc.OptionFlags |= ResourceOptionFlags.GenerateMipMaps; + desc.BindFlags |= BindFlags.RenderTarget; + } + return desc; } internal override Resource CreateTexture() @@ -403,6 +412,30 @@ namespace Microsoft.Xna.Framework.Graphics return new SharpDX.Direct3D11.Texture2D(GraphicsDevice._d3dDevice, desc); } + protected override ShaderResourceView CreateShaderResourceView() + { + var texDesc = GetTexture2DDescription(); + if (_mipmap) + { + ShaderResourceViewDescription resViewDesc = new ShaderResourceViewDescription() + { + Format = texDesc.Format, + Dimension = SharpDX.Direct3D.ShaderResourceViewDimension.Texture2D, + Texture2D = new ShaderResourceViewDescription.Texture2DResource() + { + MostDetailedMip = 0, + MipLevels = texDesc.MipLevels + } + }; + + return new SharpDX.Direct3D11.ShaderResourceView(GraphicsDevice._d3dDevice, GetTexture(), resViewDesc); + } + else + { + return base.CreateShaderResourceView(); + } + } + protected internal virtual SampleDescription CreateSampleDescription() { return new SampleDescription(1, 0); diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/Texture2D.OpenGL.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/Texture2D.OpenGL.cs index a889ff95e..6e6d04d58 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/Texture2D.OpenGL.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/Texture2D.OpenGL.cs @@ -118,6 +118,12 @@ namespace Microsoft.Xna.Framework.Graphics } GraphicsExtensions.CheckGLError(); + if (CurrentPlatform.OS != OS.MacOSX) + { + GL.GenerateMipmap(GenerateMipmapTarget.Texture2D); + GraphicsExtensions.CheckGLError(); + } + #if !ANDROID // Required to make sure that any texture uploads on a thread are completed // before the main thread tries to use the texture. diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MessageBox.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MessageBox.cs new file mode 100644 index 000000000..ed1c87dd5 --- /dev/null +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MessageBox.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Xna.Framework +{ + public static class MessageBox + { + [Flags] + public enum Flags : uint + { + Error = 0x00000010, + Warning = 0x00000020, + Information = 0x00000040 + } + + public static void Show(Flags flags, string title, string message, GameWindow window = null) + { + Sdl.ShowSimpleMessageBox((uint)flags, title, message, window?.Handle ?? IntPtr.Zero); + } + + public static void ShowWrapped(Flags flags, string title, string message, int charsPerLine = 60, GameWindow window = null) + { + string[] split = message.Split(' '); + if (split.Length > 0) + { + message = split[0]; + string currLine = message; + for (int i = 1; i < split.Length; i++) + { + currLine += " " + split[i]; + if (currLine.Length > charsPerLine) + { + currLine = split[i]; + message += "\n" + split[i]; + } + else + { + message += " " + split[i]; + } + } + } + + Show(flags, title, message, window); + } + } +} diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Linux.csproj b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Linux.csproj index a095321b9..7adda0cc3 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Linux.csproj +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Linux.csproj @@ -30,7 +30,7 @@ true - none + pdbonly ..\..\DesktopGL\ obj\Linux\AnyCPU\Release ..\..\DesktopGL\MonoGame.Framework.xml @@ -38,6 +38,7 @@ prompt 4 false + true @@ -45,6 +46,7 @@ + WindowsGL,Linux diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Windows.csproj b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Windows.csproj index c9a2d2415..2495425fd 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Windows.csproj +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Windows.csproj @@ -81,6 +81,7 @@ + Windows diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDL2.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDL2.cs index 648e035f9..5dc1e0ea4 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDL2.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/SDL/SDL2.cs @@ -255,12 +255,26 @@ internal static class Sdl public static void SetClipboardText(string text) { - byte[] bytes = Encoding.UTF8.GetBytes(text); + byte[] bytes = Encoding.UTF8.GetBytes(text+"\0"); GCHandle handle = GCHandle.Alloc(bytes, GCHandleType.Pinned); GetError(SDL_SetClipboardText(handle.AddrOfPinnedObject())); handle.Free(); } + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate int d_sdl_showsimplemessagebox(uint flags, IntPtr title, IntPtr msg, IntPtr window); + public static d_sdl_showsimplemessagebox SDL_ShowSimpleMessageBox = FuncLoader.LoadFunction(NativeLibrary, "SDL_ShowSimpleMessageBox"); + public static void ShowSimpleMessageBox(uint flags, string title, string message, IntPtr window) + { + byte[] bytesTitle = Encoding.UTF8.GetBytes(title + "\0"); + GCHandle handleTitle = GCHandle.Alloc(bytesTitle, GCHandleType.Pinned); + byte[] bytesMessage = Encoding.UTF8.GetBytes(message + "\0"); + GCHandle handleMessage = GCHandle.Alloc(bytesMessage, GCHandleType.Pinned); + GetError(SDL_ShowSimpleMessageBox(flags, handleTitle.AddrOfPinnedObject(), handleMessage.AddrOfPinnedObject(), window)); + handleTitle.Free(); + handleMessage.Free(); + } + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate IntPtr d_sdl_gethint(string name); public static d_sdl_gethint SDL_GetHint = FuncLoader.LoadFunction(NativeLibrary, "SDL_GetHint"); diff --git a/Libraries/MonoGame.Framework/Windows/MonoGame.Framework.dll b/Libraries/MonoGame.Framework/Windows/MonoGame.Framework.dll index 52de5b72a..bebe906ce 100644 Binary files a/Libraries/MonoGame.Framework/Windows/MonoGame.Framework.dll and b/Libraries/MonoGame.Framework/Windows/MonoGame.Framework.dll differ diff --git a/Libraries/MonoGame.Framework/Windows/MonoGame.Framework.pdb b/Libraries/MonoGame.Framework/Windows/MonoGame.Framework.pdb index e35e05b62..bfd0e6296 100644 Binary files a/Libraries/MonoGame.Framework/Windows/MonoGame.Framework.pdb and b/Libraries/MonoGame.Framework/Windows/MonoGame.Framework.pdb differ