diff --git a/.gitignore b/.gitignore index 2afaafc0a..4dd2d8677 100644 --- a/.gitignore +++ b/.gitignore @@ -37,5 +37,8 @@ Libraries/webm_mem_playback/opus_x64_linux/ # Mac *.DS_Store +# Win +desktop.ini + #Merge script temp.txt diff --git a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs index 48648f15b..1c5adb3ff 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs @@ -200,7 +200,7 @@ namespace Barotrauma worldView = new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight); viewMatrix = Matrix.CreateTranslation(new Vector3(GameMain.GraphicsWidth / 2.0f, GameMain.GraphicsHeight / 2.0f, 0)); - globalZoomScale = (float)Math.Pow(new Vector2(resolution.X, resolution.Y).Length() / new Vector2(1920, 1080).Length(), 2); + globalZoomScale = (float)Math.Pow(new Vector2(GUI.UIWidth, resolution.Y).Length() / GUI.ReferenceResolution.Length(), 2); } public void UpdateTransform(bool interpolate = true) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs index a9ff967ca..b773968c5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/EnemyAIController.cs @@ -35,7 +35,7 @@ namespace Barotrauma } targetPos.Y = -targetPos.Y; GUI.DrawLine(spriteBatch, pos, targetPos, GUI.Style.Red * 0.5f, 0, 4); - if (wallTarget != null) + if (wallTarget != null && (State == AIState.Attack || State == AIState.Aggressive || State == AIState.PassiveAggressive)) { Vector2 wallTargetPos = wallTarget.Position; if (wallTarget.Structure.Submarine != null) { wallTargetPos += wallTarget.Structure.Submarine.Position; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs new file mode 100644 index 000000000..fd9e93f7e --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs @@ -0,0 +1,50 @@ +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using Barotrauma.Networking; + +namespace Barotrauma +{ + partial class WreckAI : IServerSerializable + { + private CoroutineHandle fadeOutRoutine; + partial void FadeOutColors() + { + if (fadeOutRoutine != null) + { + CoroutineManager.StopCoroutines(fadeOutRoutine); + } + fadeOutRoutine = CoroutineManager.StartCoroutine(FadeOutColors(Config.DeadEntityColorFadeOutTime)); + } + + private IEnumerable FadeOutColors(float time) + { + float timer = 0; + while (timer < time) + { + timer += CoroutineManager.DeltaTime; + float m = MathHelper.Lerp(1, Config.DeadEntityColorMultiplier, MathUtils.InverseLerp(0, time, timer)); + foreach (var item in thalamusItems) + { + if (item.Prefab.BrokenSprites.None()) + { + Color c = item.prefab.SpriteColor; + item.SpriteColor = new Color(c.R / 255f * m, c.G / 255f * m, c.B / 255f * m, c.A / 255f); + } + } + foreach (var structure in thalamusStructures) + { + Color c = structure.prefab.SpriteColor; + structure.SpriteColor = new Color(c.R / 255f * m, c.G / 255f * m, c.B / 255f * m, c.A / 255f); + } + yield return CoroutineStatus.Running; + } + yield return CoroutineStatus.Success; + } + + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + { + IsAlive = msg.ReadBoolean(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs index eb622fe34..3a2159e52 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs @@ -389,7 +389,7 @@ namespace Barotrauma { foreach (Limb limb in Limbs) { - if (limb == null || limb.IsSevered || limb.ActiveSprite == null) continue; + if (limb == null || limb.IsSevered || limb.ActiveSprite == null) { continue; } Vector2 spriteOrigin = limb.ActiveSprite.Origin; spriteOrigin.X = limb.ActiveSprite.SourceRect.Width - spriteOrigin.X; @@ -404,8 +404,8 @@ namespace Barotrauma float gibParticleAmount = MathHelper.Clamp(limb.Mass / character.AnimController.Mass, 0.1f, 1.0f); foreach (ParticleEmitter emitter in character.GibEmitters) { - if (inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Air) continue; - if (!inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Water) continue; + if (inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Air) { continue; } + if (!inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Water) { continue; } emitter.Emit(1.0f, limb.WorldPosition, character.CurrentHull, amountMultiplier: gibParticleAmount); } @@ -418,7 +418,8 @@ namespace Barotrauma if (playSound) { - SoundPlayer.PlayDamageSound("Gore", 1.0f, limbJoint.LimbA.body); + var damageSound = character.GetSound(s => s.Type == CharacterSound.SoundType.Damage); + SoundPlayer.PlayDamageSound(limbJoint.Params.BreakSound, 1.0f, limbJoint.LimbA.body.DrawPosition, range: damageSound != null ? damageSound.Range : 800); } } @@ -446,9 +447,10 @@ namespace Barotrauma float depthOffset = GetDepthOffset(); for (int i = 0; i < limbs.Length; i++) { - if (depthOffset != 0.0f) { inversedLimbDrawOrder[i].ActiveSprite.Depth += depthOffset; } - inversedLimbDrawOrder[i].Draw(spriteBatch, cam, color); - if (depthOffset != 0.0f) { inversedLimbDrawOrder[i].ActiveSprite.Depth -= depthOffset; } + var limb = inversedLimbDrawOrder[i]; + if (depthOffset != 0.0f) { limb.ActiveSprite.Depth += depthOffset; } + limb.Draw(spriteBatch, cam, color); + if (depthOffset != 0.0f) { limb.ActiveSprite.Depth -= depthOffset; } } LimbJoints.ForEach(j => j.Draw(spriteBatch)); } @@ -489,8 +491,8 @@ namespace Barotrauma public void DebugDraw(SpriteBatch spriteBatch) { - if (!GameMain.DebugDraw || !character.Enabled) return; - if (simplePhysicsEnabled) return; + if (!GameMain.DebugDraw || !character.Enabled) { return; } + if (simplePhysicsEnabled) { return; } foreach (Limb limb in Limbs) { @@ -522,7 +524,10 @@ namespace Barotrauma if (limb.body.TargetPosition != null) { Vector2 pos = ConvertUnits.ToDisplayUnits((Vector2)limb.body.TargetPosition); - if (currentHull?.Submarine != null) pos += currentHull.Submarine.DrawPosition; + if (currentHull?.Submarine != null) + { + pos += currentHull.Submarine.DrawPosition; + } pos.Y = -pos.Y; GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X - 10, (int)pos.Y - 10, 20, 20), Color.Cyan, false, 0.01f); @@ -541,13 +546,19 @@ namespace Barotrauma if (character.MemState.Count > 1) { Vector2 prevPos = ConvertUnits.ToDisplayUnits(character.MemState[0].Position); - if (currentHull?.Submarine != null) prevPos += currentHull.Submarine.DrawPosition; + if (currentHull?.Submarine != null) + { + prevPos += currentHull.Submarine.DrawPosition; + } prevPos.Y = -prevPos.Y; for (int i = 1; i < character.MemState.Count; i++) { Vector2 currPos = ConvertUnits.ToDisplayUnits(character.MemState[i].Position); - if (currentHull?.Submarine != null) currPos += currentHull.Submarine.DrawPosition; + if (currentHull?.Submarine != null) + { + currPos += currentHull.Submarine.DrawPosition; + } currPos.Y = -currPos.Y; GUI.DrawRectangle(spriteBatch, new Rectangle((int)currPos.X - 3, (int)currPos.Y - 3, 6, 6), Color.Cyan * 0.6f, true, 0.01f); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 7522954e5..c6a2f74d3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -10,6 +10,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Barotrauma.Extensions; namespace Barotrauma { @@ -233,7 +234,7 @@ namespace Barotrauma pressureParticleTimer += pressure * deltaTime; if (pressureParticleTimer > 10.0f) { - Particle p = GameMain.ParticleManager.CreateParticle("waterblood", WorldPosition + Rand.Vector(5.0f), Rand.Vector(10.0f)); + GameMain.ParticleManager.CreateParticle(Params.BleedParticleWater, WorldPosition + Rand.Vector(5.0f), Rand.Vector(10.0f)); pressureParticleTimer = 0.0f; } } @@ -355,7 +356,7 @@ namespace Barotrauma } } - partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult) + partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult, float stun) { if (attackResult.Damage <= 1.0f || IsDead) { return; } if (soundTimer < soundInterval * 0.5f) @@ -444,6 +445,7 @@ namespace Barotrauma if (item.body != null && !item.body.Enabled) continue; if (item.ParentInventory != null) continue; if (ignoredItems != null && ignoredItems.Contains(item)) continue; + if (Screen.Selected is SubEditorScreen editor && editor.WiringMode && item.GetComponent() == null) { continue; } if (draggingItemToWorld) { @@ -812,19 +814,23 @@ namespace Barotrauma return progressBar; } + private readonly List matchingSounds = new List(); private SoundChannel soundChannel; public void PlaySound(CharacterSound.SoundType soundType) { if (sounds == null || sounds.Count == 0) { return; } if (soundChannel != null && soundChannel.IsPlaying) { return; } - - var matchingSounds = sounds.Where(s => - s.Type == soundType && - (s.Gender == Gender.None || (info != null && info.Gender == s.Gender))); - if (!matchingSounds.Any()) { return; } - - var matchingSoundsList = matchingSounds.ToList(); - var selectedSound = matchingSoundsList[Rand.Int(matchingSoundsList.Count)]; + if (GameMain.SoundManager?.Disabled ?? true) { return; } + matchingSounds.Clear(); + foreach (var s in sounds) + { + if (s.Type == soundType && (s.Gender == Gender.None || (info != null && info.Gender == s.Gender))) + { + matchingSounds.Add(s); + } + } + var selectedSound = matchingSounds.GetRandom(); + if (selectedSound?.Sound == null) { return; } soundChannel = SoundPlayer.PlaySound(selectedSound.Sound, AnimController.WorldPosition, selectedSound.Volume, selectedSound.Range, CurrentHull); soundTimer = soundInterval; } @@ -843,6 +849,11 @@ namespace Barotrauma activeObjectiveEntities.Remove(found); } + /// + /// Note that when a predicate is provided, the random option uses Linq.Where() extension method, which creates a new collection. + /// + public CharacterSound GetSound(Func predicate = null, bool random = false) => random ? sounds.GetRandom(predicate) : sounds.FirstOrDefault(predicate); + partial void ImplodeFX() { Vector2 centerOfMass = AnimController.GetCenterOfMass(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index 8ccf31dcc..f9ce57161 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -2,6 +2,7 @@ using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; using System.Linq; @@ -28,7 +29,7 @@ namespace Barotrauma { if (hudFrame == null) { - hudFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), style: null) + hudFrame = new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas), style: null) { CanBeFocused = false }; @@ -37,6 +38,9 @@ namespace Barotrauma } } + private static bool shouldRecreateHudTexts = true; + private static bool heldDownShiftWhenGotHudTexts; + private static bool ShouldDrawInventory(Character character) { return @@ -140,7 +144,11 @@ namespace Barotrauma else { focusedItemOverlayTimer = Math.Max(focusedItemOverlayTimer - deltaTime, 0.0f); - if (focusedItemOverlayTimer <= 0.0f) focusedItem = null; + if (focusedItemOverlayTimer <= 0.0f) + { + focusedItem = null; + shouldRecreateHudTexts = true; + } } } @@ -194,6 +202,7 @@ namespace Barotrauma foreach (Item brokenItem in brokenItems) { + if (brokenItem.NonInteractable) { continue; } float dist = Vector2.Distance(character.WorldPosition, brokenItem.WorldPosition); Vector2 drawPos = brokenItem.DrawPosition; float alpha = Math.Min((1000.0f - dist) / 1000.0f * 2.0f, 1.0f); @@ -209,20 +218,20 @@ namespace Barotrauma DrawCharacterHoverTexts(spriteBatch, cam, character); } - float circleSize; if (character.FocusedItem != null) { if (focusedItem != character.FocusedItem) { focusedItemOverlayTimer = Math.Min(1.0f, focusedItemOverlayTimer); + shouldRecreateHudTexts = true; } - focusedItem = character.FocusedItem; + focusedItem = character.FocusedItem; } if (focusedItem != null && focusedItemOverlayTimer > ItemOverlayDelay) { Vector2 circlePos = cam.WorldToScreen(focusedItem.DrawPosition); - circleSize = Math.Max(focusedItem.Rect.Width, focusedItem.Rect.Height) * 1.5f; + float circleSize = Math.Max(focusedItem.Rect.Width, focusedItem.Rect.Height) * 1.5f; circleSize = MathHelper.Clamp(circleSize, 45.0f, 100.0f) * Math.Min((focusedItemOverlayTimer - 1.0f) * 5.0f, 1.0f); if (circleSize > 0.0f) { @@ -238,7 +247,14 @@ namespace Barotrauma if (!GUI.DisableItemHighlights && !Inventory.DraggingItemToWorld) { - var hudTexts = focusedItem.GetHUDTexts(character); + bool shiftDown = PlayerInput.KeyDown(Keys.LeftShift) || PlayerInput.KeyDown(Keys.RightShift); + if(shouldRecreateHudTexts || heldDownShiftWhenGotHudTexts != shiftDown) + { + shouldRecreateHudTexts = true; + heldDownShiftWhenGotHudTexts = shiftDown; + } + var hudTexts = focusedItem.GetHUDTexts(character, shouldRecreateHudTexts); + shouldRecreateHudTexts = false; int dir = Math.Sign(focusedItem.WorldPosition.X - character.WorldPosition.X); @@ -358,7 +374,7 @@ namespace Barotrauma { GUIComponent.DrawToolTip( spriteBatch, - character.Info?.Job == null ? character.DisplayName : character.Name + " (" + character.Info.Job.Name + ")", + character.Info?.Job == null ? character.DisplayName : character.DisplayName + " (" + character.Info.Job.Name + ")", HUDLayoutSettings.PortraitArea); } } @@ -378,10 +394,6 @@ namespace Barotrauma startPos = cam.WorldToScreen(startPos); string focusName = character.FocusedCharacter.DisplayName; - if (character.FocusedCharacter.Info != null) - { - focusName = character.FocusedCharacter.Info.DisplayName; - } Vector2 textPos = startPos; Vector2 textSize = GUI.Font.MeasureString(focusName); Vector2 largeTextSize = GUI.SubHeadingFont.MeasureString(focusName); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index 4a93fc785..8058762f4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -19,75 +19,107 @@ namespace Barotrauma } - public GUIFrame CreateInfoFrame(GUIFrame frame) + public GUIComponent CreateInfoFrame(GUIFrame frame, bool returnParent, Sprite permissionIcon = null) { - var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), frame.RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.1f) }) + var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.874f, 0.58f), frame.RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.05f) }) { - Stretch = true, - RelativeSpacing = 0.03f + RelativeSpacing = 0.05f + //Stretch = true }; - var headerArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.4f), paddedFrame.RectTransform), isHorizontal: true) - { - RelativeSpacing = 0.05f, - Stretch = true - }; + var headerArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.322f), paddedFrame.RectTransform), isHorizontal: true); - new GUICustomComponent(new RectTransform(new Vector2(0.25f, 1.0f), headerArea.RectTransform), - onDraw: (sb, component) => DrawIcon(sb, component.Rect.Center.ToVector2(), targetAreaSize: component.Rect.Size.ToVector2())); + new GUICustomComponent(new RectTransform(new Vector2(0.425f, 1.0f), headerArea.RectTransform), + onDraw: (sb, component) => DrawInfoFrameCharacterIcon(sb, component.Rect)); ScalableFont font = paddedFrame.Rect.Width < 280 ? GUI.SmallFont : GUI.Font; - var headerTextArea = new GUILayoutGroup(new RectTransform(new Vector2(0.75f, 1.0f), headerArea.RectTransform)) + var headerTextArea = new GUILayoutGroup(new RectTransform(new Vector2(0.575f, 1.0f), headerArea.RectTransform)) { - RelativeSpacing = 0.05f, + RelativeSpacing = 0.02f, Stretch = true }; Color? nameColor = null; if (Job != null) { nameColor = Job.Prefab.UIColor; } - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), - Name, textColor: nameColor, font: GUI.LargeFont) + + GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), ToolBox.LimitString(Name, GUI.Font, headerTextArea.Rect.Width), textColor: nameColor, font: GUI.Font) { - Padding = Vector4.Zero, - AutoScaleHorizontal = true + ForceUpperCase = true, + Padding = Vector4.Zero }; - if (Job != null) + if (permissionIcon != null) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), - Job.Name, textColor: Job.Prefab.UIColor, font: font); + Point iconSize = permissionIcon.SourceRect.Size; + int iconWidth = (int)((float)characterNameBlock.Rect.Height / iconSize.Y * iconSize.X); + new GUIImage(new RectTransform(new Point(iconWidth, characterNameBlock.Rect.Height), characterNameBlock.RectTransform) { AbsoluteOffset = new Point(-iconWidth - 2, 0) }, permissionIcon) { IgnoreLayoutGroups = true }; + } + + if (Job != null) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), Job.Name, textColor: Job.Prefab.UIColor, font: font) + { + Padding = Vector4.Zero + }; } 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); + 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) + { + Padding = Vector4.Zero + }; } - //spacing - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), paddedFrame.RectTransform), style: null); - - if (Job != null) + if (Job != null && (Character == null || !Character.IsDead)) { + var skillsArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.63f), paddedFrame.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter)) + { + Stretch = true + }; + var skills = Job.Skills; skills.Sort((s1, s2) => -s1.Level.CompareTo(s2.Level)); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), - TextManager.Get("Skills") + ":", font: font); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillsArea.RectTransform), TextManager.AddPunctuation(':', TextManager.Get("skills"), string.Empty), font: font) { Padding = Vector4.Zero }; foreach (Skill skill in skills) { Color textColor = Color.White * (0.5f + skill.Level / 200.0f); - var skillName = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), - TextManager.Get("SkillName." + skill.Identifier), textColor: textColor, font: font); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), skillName.RectTransform), - ((int)skill.Level).ToString(), textColor: textColor, font: font, textAlignment: Alignment.CenterRight); + var skillName = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillsArea.RectTransform), TextManager.Get("SkillName." + skill.Identifier), textColor: textColor, font: font) { Padding = Vector4.Zero }; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), skillName.RectTransform), ((int)skill.Level).ToString(), textColor: textColor, font: font, textAlignment: Alignment.CenterRight); } } + else if (Character != null && Character.IsDead) + { + var deadArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.63f), paddedFrame.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter)) + { + Stretch = true + }; - return frame; + string deadDescription = TextManager.AddPunctuation(':', TextManager.Get("deceased") + "\n" + Character.CauseOfDeath.Affliction?.CauseOfDeathDescription ?? + TextManager.AddPunctuation(':', TextManager.Get("CauseOfDeath"), TextManager.Get("CauseOfDeath." + Character.CauseOfDeath.Type.ToString()))); + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), deadArea.RectTransform), deadDescription, textColor: GUI.Style.Red, font: font, textAlignment: Alignment.TopLeft) { Padding = Vector4.Zero }; + } + + if (returnParent) + { + return frame; + } + else + { + return paddedFrame; + } + } + + private void DrawInfoFrameCharacterIcon(SpriteBatch sb, Rectangle componentRect) + { + Vector2 targetAreaSize = componentRect.Size.ToVector2(); + float scale = Math.Min(targetAreaSize.X / headSprite.size.X, targetAreaSize.Y / headSprite.size.Y); + DrawIcon(sb, componentRect.Location.ToVector2() + headSprite.size / 2 * scale, targetAreaSize); } public GUIFrame CreateCharacterFrame(GUIComponent parent, string text, object userData) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 5726215f5..8a815e11f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -278,7 +278,7 @@ namespace Barotrauma break; case ServerNetObject.ENTITY_EVENT: - int eventType = msg.ReadRangedInteger(0, 3); + int eventType = msg.ReadRangedInteger(0, 4); switch (eventType) { case 0: @@ -338,6 +338,40 @@ namespace Barotrauma info?.SetSkillLevel(skillIdentifier, skillLevel, WorldPosition + Vector2.UnitY * 150.0f); } break; + case 4: + int attackLimbIndex = msg.ReadByte(); + UInt16 targetEntityID = msg.ReadUInt16(); + int targetLimbIndex = msg.ReadByte(); + + //255 = entity already removed, no need to do anything + if (attackLimbIndex == 255) { break; } + + if (attackLimbIndex >= AnimController.Limbs.Length) + { + DebugConsole.ThrowError($"Received invalid ExecuteAttack message. Limb index out of bounds ({attackLimbIndex})"); + break; + } + Limb attackLimb = AnimController.Limbs[attackLimbIndex]; + Limb targetLimb = null; + if (!(FindEntityByID(targetEntityID) is IDamageable targetEntity)) + { + DebugConsole.ThrowError($"Received invalid ExecuteAttack message. Target entity not found (ID {targetEntityID})"); + break; + } + if (targetEntity is Character targetCharacter) + { + if (targetLimbIndex >= targetCharacter.AnimController.Limbs.Length) + { + DebugConsole.ThrowError($"Received invalid ExecuteAttack message. Target limb index out of bounds ({targetLimbIndex})"); + break; + } + targetLimb = targetCharacter.AnimController.Limbs[targetLimbIndex]; + } + if (attackLimb?.attack != null) + { + attackLimb.ExecuteAttack(targetEntity, targetLimb, out _); + } + break; } msg.ReadPadBits(); break; @@ -397,8 +431,7 @@ namespace Barotrauma if (orderPrefabIndex >= 0 && orderPrefabIndex < Order.PrefabList.Count) { var orderPrefab = Order.PrefabList[orderPrefabIndex]; - if ((orderPrefab.ItemComponentType == null && orderPrefab.ItemIdentifiers.None()) || - (targetEntity != null && (targetEntity as Item).Components.Any(c => c?.GetType() == orderPrefab.ItemComponentType))) + if (!orderPrefab.MustSetTarget || (targetEntity != null && (targetEntity as Item).Components.Any(c => c?.GetType() == orderPrefab.ItemComponentType))) { character.SetOrder( new Order(orderPrefab, targetEntity, (targetEntity as Item)?.Components.FirstOrDefault(c => c?.GetType() == orderPrefab.ItemComponentType), orderGiver: orderGiver), @@ -469,11 +502,9 @@ namespace Barotrauma causeOfDeathAffliction = AfflictionPrefab.Prefabs[afflictionName]; } } - - byte severedLimbCount = msg.ReadByte(); if (!IsDead) { - if (causeOfDeathType == CauseOfDeathType.Pressure) + if (causeOfDeathType == CauseOfDeathType.Pressure || causeOfDeathAffliction == AfflictionPrefab.Pressure) { Implode(true); } @@ -482,26 +513,26 @@ namespace Barotrauma Kill(causeOfDeathType, causeOfDeathAffliction?.Instantiate(1.0f), true); } } - - for (int i = 0; i < severedLimbCount; i++) - { - int severedJointIndex = msg.ReadByte(); - if (severedJointIndex < 0 || severedJointIndex >= AnimController.LimbJoints.Length) - { - string errorMsg = $"Error in CharacterNetworking.ReadStatus: severed joint index out of bounds (index: {severedJointIndex}, joint count: {AnimController.LimbJoints.Length})"; - GameAnalyticsManager.AddErrorEventOnce("CharacterNetworking.ReadStatus:JointIndexOutOfBounts", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); - } - else - { - AnimController.SeverLimbJoint(AnimController.LimbJoints[severedJointIndex]); - } - } } else { if (IsDead) { Revive(); } CharacterHealth.ClientRead(msg); } + byte severedLimbCount = msg.ReadByte(); + for (int i = 0; i < severedLimbCount; i++) + { + int severedJointIndex = msg.ReadByte(); + if (severedJointIndex < 0 || severedJointIndex >= AnimController.LimbJoints.Length) + { + string errorMsg = $"Error in CharacterNetworking.ReadStatus: severed joint index out of bounds (index: {severedJointIndex}, joint count: {AnimController.LimbJoints.Length})"; + GameAnalyticsManager.AddErrorEventOnce("CharacterNetworking.ReadStatus:JointIndexOutOfBounts", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + } + else + { + AnimController.SeverLimbJoint(AnimController.LimbJoints[severedJointIndex]); + } + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs index 1ba700e72..cbef93fdc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs @@ -1,6 +1,6 @@ using Microsoft.Xna.Framework; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionPsychosis.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionPsychosis.cs index c6a8139fc..03ab84938 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionPsychosis.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionPsychosis.cs @@ -1,4 +1,5 @@ using Barotrauma.Extensions; +using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -20,9 +21,9 @@ namespace Barotrauma } const int MaxFakeFireSources = 10; - private float minFakeFireSourceInterval = 10.0f, maxFakeFireSourceInterval = 200.0f; + const float MinFakeFireSourceInterval = 30.0f, MaxFakeFireSourceInterval = 240.0f; private float createFireSourceTimer; - private List fakeFireSources = new List(); + private readonly List fakeFireSources = new List(); enum FloodType { @@ -31,26 +32,30 @@ namespace Barotrauma HideFlooding } - private float minSoundInterval = 10.0f, maxSoundInterval = 60.0f; + const float MinSoundInterval = 10.0f, MaxSoundInterval = 180.0f; private FloodType currentFloodType; private float soundTimer; - private float minFloodInterval = 30.0f, maxFloodInterval = 180.0f; + const float MinFloodInterval = 60.0f, MaxFloodInterval = 240.0f; private float createFloodTimer; private float currentFloodState; private float currentFloodDuration; + private float fakeBrokenInterval = 30.0f; + private float fakeBrokenTimer = 0.0f; + partial void UpdateProjSpecific(CharacterHealth characterHealth, Limb targetLimb, float deltaTime) { if (Character.Controlled != characterHealth.Character) return; UpdateFloods(deltaTime); UpdateSounds(characterHealth.Character, deltaTime); - UpdateFires(characterHealth.Character, deltaTime); + UpdateFires(characterHealth.Character, deltaTime); + UpdateFakeBroken(deltaTime); } private void UpdateSounds(Character character, float deltaTime) { - if (soundTimer < MathHelper.Lerp(maxSoundInterval, minSoundInterval, Strength / 100.0f)) + if (soundTimer < MathHelper.Lerp(MaxSoundInterval, MinSoundInterval, Strength / 100.0f)) { soundTimer += deltaTime; return; @@ -97,7 +102,7 @@ namespace Barotrauma return; } - if (createFloodTimer < MathHelper.Lerp(maxFloodInterval, minFloodInterval, Strength / 100.0f)) + if (createFloodTimer < MathHelper.Lerp(MaxFloodInterval, MinFloodInterval, Strength / 100.0f)) { createFloodTimer += deltaTime; return; @@ -124,7 +129,7 @@ namespace Barotrauma createFireSourceTimer += deltaTime; if (fakeFireSources.Count < MaxFakeFireSources && character.Submarine != null && - createFireSourceTimer > MathHelper.Lerp(maxFakeFireSourceInterval, minFakeFireSourceInterval, Strength / 100.0f)) + createFireSourceTimer > MathHelper.Lerp(MaxFakeFireSourceInterval, MinFakeFireSourceInterval, Strength / 100.0f)) { Hull fireHull = Hull.hullList.GetRandom(h => h.Submarine == character.Submarine); @@ -140,9 +145,9 @@ namespace Barotrauma foreach (FakeFireSource fakeFireSource in fakeFireSources) { - if (fakeFireSource.Hull.Surface > fakeFireSource.Hull.Rect.Y - fakeFireSource.Hull.Rect.Height + fakeFireSource.Position.Y) + if (fakeFireSource.Hull.DrawSurface > fakeFireSource.Hull.Rect.Y - fakeFireSource.Hull.Rect.Height + fakeFireSource.Position.Y) { - fakeFireSource.LifeTime -= deltaTime * 10.0f; + fakeFireSource.LifeTime -= deltaTime * 100.0f; } fakeFireSource.LifeTime -= deltaTime; @@ -162,5 +167,28 @@ namespace Barotrauma fakeFireSources.RemoveAll(fs => fs.LifeTime <= 0.0f); } + + private void UpdateFakeBroken(float deltaTime) + { + fakeBrokenTimer -= deltaTime; + if (fakeBrokenTimer > 0.0f) { return; } + + foreach (Item item in Item.ItemList) + { + var repairable = item.GetComponent(); + if (repairable == null) { continue; } + if (ShouldFakeBrokenItem(item)) + { + repairable.FakeBrokenTimer = 60.0f; + } + } + + fakeBrokenTimer = fakeBrokenInterval; + } + + private bool ShouldFakeBrokenItem(Item item) + { + return Rand.Range(0.0f, 1000.0f) < Strength; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index f2aaecdd4..54c1ba354 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -251,8 +251,7 @@ namespace Barotrauma { get { - // 0.38775510204f = percentage of offset before reaching the healthbar portion of the graphic going from bottom upwards - return new Point(2, (int)(HUDLayoutSettings.HealthBarArea.Size.Y * 0.38775510204f)); + return new Point(Math.Max(2, GUI.IntScaleCeiling(1.5f)), Math.Min(GUI.IntScaleFloor(18f), 19)); } } @@ -260,7 +259,7 @@ namespace Barotrauma { get { - return new Point((int)Math.Ceiling(HUDLayoutSettings.HealthBarArea.Size.X - 45 * GUI.Scale), (int)(healthBarHolder.Rect.Height - Math.Min(23 * GUI.Scale, 25)) / 2); + return new Point(healthBarHolder.Rect.Width - Math.Min(GUI.IntScale(45f), 47), GUI.IntScale(15f)); } } @@ -505,8 +504,6 @@ namespace Barotrauma Character.Controlled.AnimController.Anim = (Character.Controlled.AnimController.Anim == AnimController.Animation.CPR) ? AnimController.Animation.None : AnimController.Animation.CPR; - button.Selected = Character.Controlled.AnimController.Anim == AnimController.Animation.CPR; - selectedCharacter.AnimController.ResetPullJoints(); if (GameMain.Client != null) @@ -599,12 +596,14 @@ namespace Barotrauma switch (alignment) { case Alignment.Left: - healthInterfaceFrame.RectTransform.SetPosition(Anchor.CenterLeft); + healthInterfaceFrame.RectTransform.SetPosition(Anchor.BottomLeft); break; case Alignment.Right: - healthInterfaceFrame.RectTransform.SetPosition(Anchor.CenterRight); + healthInterfaceFrame.RectTransform.SetPosition(Anchor.BottomRight); break; } + + healthInterfaceFrame.RectTransform.AbsoluteOffset = new Point(HUDLayoutSettings.Padding, screenResolution.Y - HUDLayoutSettings.ChatBoxArea.Y + HUDLayoutSettings.Padding); healthInterfaceFrame.RectTransform.RecalculateChildren(false); } @@ -651,10 +650,14 @@ namespace Barotrauma bloodParticleTimer -= deltaTime * (affliction.Strength / 10.0f); if (bloodParticleTimer <= 0.0f) { + bool inWater = Character.AnimController.InWater; float bloodParticleSize = MathHelper.Lerp(0.5f, 1.0f, affliction.Strength / 100.0f); - if (!Character.AnimController.InWater) bloodParticleSize *= 2.0f; + if (!inWater) + { + bloodParticleSize *= 2.0f; + } var blood = GameMain.ParticleManager.CreateParticle( - Character.AnimController.InWater ? "waterblood" : "blooddrop", + inWater ? Character.Params.BleedParticleWater : Character.Params.BleedParticleAir, targetLimb.WorldPosition, Rand.Vector(affliction.Strength), 0.0f, Character.AnimController.CurrentHull); if (blood != null) @@ -765,7 +768,8 @@ namespace Barotrauma { OpenHealthWindow = null; } - else if (Character.Controlled == Character && Character.Controlled.FocusedCharacter == null) + else if (Character.Controlled == Character && + (Character.Controlled.FocusedCharacter?.CharacterHealth == null || !Character.Controlled.FocusedCharacter.CharacterHealth.UseHealthWindow)) { OpenHealthWindow = this; forceAfflictionContainerUpdate = true; @@ -946,6 +950,10 @@ namespace Barotrauma && !Character.IsDead && openHealthWindow == this; cprButton.IgnoreLayoutGroups = !cprButton.Visible; + cprButton.Selected = + Character.Controlled != null && + Character == Character.Controlled.SelectedCharacter && + Character.Controlled.AnimController.Anim == AnimController.Animation.CPR; cprFrame.RectTransform.Resize(new Vector2(0.7f, 1.0f)); cprButton.RectTransform.Resize(new Vector2(1.0f, 1.0f)); @@ -1100,7 +1108,7 @@ namespace Barotrauma float currHealth = healthBar.BarSize; Color prevColor = healthBar.Color; healthBarShadow.BarSize = healthShadowSize; - healthBarShadow.Color = GUI.Style.Red; + healthBarShadow.Color = Color.Lerp(GUI.Style.Red, Color.Black, 0.5f); healthBarShadow.Visible = true; healthBar.BarSize = currHealth; healthBar.Color = prevColor; @@ -1815,7 +1823,7 @@ namespace Barotrauma Vector2 iconPos = highlightArea.Center.ToVector2(); //Affliction mostSevereAffliction = thisAfflictions.FirstOrDefault(a => !a.Prefab.IsBuff && !thisAfflictions.Any(a2 => !a2.Prefab.IsBuff && a2.Strength > a.Strength)) ?? thisAfflictions.FirstOrDefault(); - Affliction mostSevereAffliction = SortAfflictionsBySeverity(thisAfflictions).FirstOrDefault(); + Affliction mostSevereAffliction = SortAfflictionsBySeverity(thisAfflictions, excludeBuffs: false).FirstOrDefault(); if (mostSevereAffliction != null) { DrawLimbAfflictionIcon(spriteBatch, mostSevereAffliction, iconScale, ref iconPos); } if (thisAfflictions.Count() > 1) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs index feb8dee03..ad9f5ff23 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Jobs/JobPrefab.cs @@ -12,8 +12,10 @@ namespace Barotrauma { int width = 500, height = 400; - GUIButton backFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas), style: "GUIBackgroundBlocker"); - GUIFrame frame = new GUIFrame(new RectTransform(new Point(width, height), backFrame.RectTransform, Anchor.Center)); + GUIButton frameHolder = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null); + new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, frameHolder.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); + + GUIFrame frame = new GUIFrame(new RectTransform(new Point(width, height), frameHolder.RectTransform, Anchor.Center)); GUIFrame paddedFrame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), frame.RectTransform, Anchor.Center), style: null); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), paddedFrame.RectTransform), Name, font: GUI.LargeFont); @@ -49,7 +51,7 @@ namespace Barotrauma font: GUI.SmallFont); }*/ - return backFrame; + return frameHolder; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index fea4134be..e44ed6055 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -8,7 +8,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; using SpriteParams = Barotrauma.RagdollParams.SpriteParams; @@ -70,7 +70,6 @@ namespace Barotrauma } } } - } public void Draw(SpriteBatch spriteBatch) @@ -110,6 +109,7 @@ namespace Barotrauma private float wetTimer; private float dripParticleTimer; + private float deadTimer; /// /// Note that different limbs can share the same deformations. @@ -125,7 +125,7 @@ namespace Barotrauma { get { - var conditionalSprite = ConditionalSprites.FirstOrDefault(c => c.IsActive && c.DeformableSprite != null); + var conditionalSprite = ConditionalSprites.FirstOrDefault(c => c.Exclusive && c.IsActive && c.DeformableSprite != null); if (conditionalSprite != null) { return conditionalSprite.DeformableSprite; @@ -143,7 +143,7 @@ namespace Barotrauma { get { - var conditionalSprite = ConditionalSprites.FirstOrDefault(c => c.IsActive && c.ActiveSprite != null); + var conditionalSprite = ConditionalSprites.FirstOrDefault(c => c.Exclusive && c.IsActive && c.ActiveSprite != null); if (conditionalSprite != null) { return conditionalSprite.ActiveSprite; @@ -165,6 +165,12 @@ namespace Barotrauma public Sprite DamagedSprite { get; private set; } + public bool Hide + { + get => Params.Hide; + set => Params.Hide = value; + } + public List ConditionalSprites { get; private set; } = new List(); private Dictionary spriteAnimState = new Dictionary(); private Dictionary> DecorativeSpriteGroups = new Dictionary>(); @@ -273,7 +279,17 @@ namespace Barotrauma DamagedSprite = new Sprite(subElement, file: GetSpritePath(subElement, Params.damagedSpriteParams)); break; case "conditionalsprite": - var conditionalSprite = new ConditionalSprite(subElement, character, file: GetSpritePath(subElement, null)); + ISerializableEntity targetEntity; + string target = subElement.GetAttributeString("target", null); + if (string.Equals(target, "character", StringComparison.OrdinalIgnoreCase)) + { + targetEntity = character; + } + else + { + targetEntity = this; + } + var conditionalSprite = new ConditionalSprite(subElement, targetEntity, file: GetSpritePath(subElement, null)); ConditionalSprites.Add(conditionalSprite); if (conditionalSprite.DeformableSprite != null) { @@ -372,12 +388,16 @@ namespace Barotrauma private string GetSpritePath(XElement element, SpriteParams spriteParams) { - string texturePath = element.GetAttributeString("texture", null); - if (string.IsNullOrWhiteSpace(texturePath) && spriteParams != null) + if (spriteParams != null) { - texturePath = spriteParams.Ragdoll.Texture; + return GetSpritePath(spriteParams.GetTexturePath()); + } + else + { + string texturePath = element.GetAttributeString("texture", null); + texturePath = string.IsNullOrWhiteSpace(texturePath) ? ragdoll.RagdollParams.Texture : texturePath; + return GetSpritePath(texturePath); } - return GetSpritePath(texturePath); } /// @@ -418,19 +438,46 @@ namespace Barotrauma } } - partial void AddDamageProjSpecific(Vector2 simPosition, List afflictions, bool playSound, List appliedDamageModifiers) + partial void AddDamageProjSpecific(bool playSound, AttackResult result) { - float bleedingDamage = character.CharacterHealth.DoesBleed ? afflictions.FindAll(a => a is AfflictionBleeding).Sum(a => a.GetVitalityDecrease(character.CharacterHealth)) : 0; - float damage = afflictions.FindAll(a => a.Prefab.AfflictionType == "damage").Sum(a => a.GetVitalityDecrease(character.CharacterHealth)); - float damageMultiplier = 1; - foreach (DamageModifier damageModifier in appliedDamageModifiers) + float bleedingDamage = 0; + if (character.CharacterHealth.DoesBleed) { - damageMultiplier *= damageModifier.DamageMultiplier; + foreach (var affliction in result.Afflictions) + { + if (affliction is AfflictionBleeding) + { + bleedingDamage += affliction.GetVitalityDecrease(character.CharacterHealth); + } + } + } + float damage = 0; + foreach (var affliction in result.Afflictions) + { + if (affliction.Prefab.AfflictionType == "damage") + { + damage += affliction.GetVitalityDecrease(character.CharacterHealth); + } + } + float damageMultiplier = 1; + foreach (DamageModifier damageModifier in result.AppliedDamageModifiers) + { + foreach (var afflictionPrefab in AfflictionPrefab.List) + { + if (damageModifier.MatchesAffliction(afflictionPrefab.Identifier, afflictionPrefab.AfflictionType)) + { + if (afflictionPrefab.Effects.Any(e => e.MaxVitalityDecrease > 0)) + { + damageMultiplier *= damageModifier.DamageMultiplier; + break; + } + } + } } if (playSound) { string damageSoundType = (bleedingDamage > damage) ? "LimbSlash" : "LimbBlunt"; - foreach (DamageModifier damageModifier in appliedDamageModifiers) + foreach (DamageModifier damageModifier in result.AppliedDamageModifiers) { if (!string.IsNullOrWhiteSpace(damageModifier.DamageSound)) { @@ -447,9 +494,8 @@ namespace Barotrauma { foreach (ParticleEmitter emitter in character.DamageEmitters) { - if (inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Air) continue; - if (!inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Water) continue; - + if (inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Air) { continue; } + if (!inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Water) { continue; } emitter.Emit(1.0f, WorldPosition, character.CurrentHull, amountMultiplier: damageParticleAmount); } } @@ -461,9 +507,8 @@ namespace Barotrauma foreach (ParticleEmitter emitter in character.BloodEmitters) { - if (inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Air) continue; - if (!inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Water) continue; - + if (inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Air) { continue; } + if (!inWater && emitter.Prefab.ParticlePrefab.DrawTarget == ParticlePrefab.DrawTargetType.Water) { continue; } emitter.Emit(1.0f, WorldPosition, character.CurrentHull, sizeMultiplier: bloodParticleSize, amountMultiplier: bloodParticleAmount); } @@ -471,19 +516,26 @@ namespace Barotrauma { character.CurrentHull.AddDecal(character.BloodDecalName, WorldPosition, MathHelper.Clamp(bloodParticleSize, 0.5f, 1.0f)); } - } - + } } partial void UpdateProjSpecific(float deltaTime) { - if (!body.Enabled) return; + if (!body.Enabled) { return; } - if (!character.IsDead) + if (!IsDead) { DamageOverlayStrength -= deltaTime; BurnOverlayStrength -= deltaTime; } + else + { + var spriteParams = Params.GetSprite(); + if (spriteParams.DeadColorTime > 0 && deadTimer < spriteParams.DeadColorTime) + { + deadTimer += deltaTime; + } + } if (inWater) { @@ -524,7 +576,14 @@ namespace Barotrauma public void Draw(SpriteBatch spriteBatch, Camera cam, Color? overrideColor = null) { float brightness = 1.0f - (burnOverLayStrength / 100.0f) * 0.5f; - Color color = new Color(brightness, brightness, brightness); + var spriteParams = Params.GetSprite(); + if (spriteParams == null) { return; } + + Color color = new Color(spriteParams.Color.R / 255f * brightness, spriteParams.Color.G / 255f * brightness, spriteParams.Color.B / 255f * brightness, spriteParams.Color.A / 255f); + if (deadTimer > 0) + { + color = Color.Lerp(color, spriteParams.DeadColor, MathUtils.InverseLerp(0, spriteParams.DeadColorTime, deadTimer)); + } color = overrideColor ?? color; @@ -545,7 +604,7 @@ namespace Barotrauma float herpesStrength = character.CharacterHealth.GetAfflictionStrength("spaceherpes"); - bool hideLimb = Params.Hide || + bool hideLimb = Hide || OtherWearables.Any(w => w.HideLimb) || wearingItems.Any(w => w != null && w.HideLimb); @@ -566,6 +625,7 @@ namespace Barotrauma { var deformation = SpriteDeformation.GetDeformation(Deformations, deformSprite.Size); deformSprite.Deform(deformation); + LightSource?.DeformableLightSprite?.Deform(deformation); } else { @@ -577,6 +637,31 @@ namespace Barotrauma { body.Draw(spriteBatch, activeSprite, color, null, Scale * TextureScale, Params.MirrorHorizontally, Params.MirrorVertically); } + // Handle non-exlusive, i.e. additional conditional sprites + foreach (var conditionalSprite in ConditionalSprites) + { + // Exclusive conditional sprites are handled in the Properties + if (conditionalSprite.Exclusive) { continue; } + if (!conditionalSprite.IsActive) { continue; } + if (conditionalSprite.DeformableSprite != null) + { + var defSprite = conditionalSprite.DeformableSprite; + if (Deformations != null && Deformations.Any()) + { + var deformation = SpriteDeformation.GetDeformation(Deformations, defSprite.Size); + defSprite.Deform(deformation); + } + else + { + defSprite.Reset(); + } + body.Draw(defSprite, cam, Vector2.One * Scale * TextureScale, color, Params.MirrorHorizontally); + } + else + { + body.Draw(spriteBatch, conditionalSprite.Sprite, color, null, Scale * TextureScale, Params.MirrorHorizontally, Params.MirrorVertically); + } + } } SpriteEffects spriteEffect = (dir == Direction.Right) ? SpriteEffects.None : SpriteEffects.FlipHorizontally; if (LightSource != null) @@ -594,13 +679,19 @@ namespace Barotrauma foreach (var decorativeSprite in DecorativeSprites) { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } + Color c = new Color(decorativeSprite.Color.R / 255f * brightness, decorativeSprite.Color.G / 255f * brightness, decorativeSprite.Color.B / 255f * brightness, decorativeSprite.Color.A / 255f); + if (deadTimer > 0) + { + c = Color.Lerp(c, spriteParams.DeadColor, MathUtils.InverseLerp(0, Params.GetSprite().DeadColorTime, deadTimer)); + } + c = overrideColor ?? c; float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState); Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState) * Scale; var ca = (float)Math.Cos(-body.Rotation); var sa = (float)Math.Sin(-body.Rotation); Vector2 transformedOffset = new Vector2(ca * offset.X + sa * offset.Y, -sa * offset.X + ca * offset.Y); - decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(body.DrawPosition.X + transformedOffset.X, -(body.DrawPosition.Y + transformedOffset.Y)), color, - -body.Rotation + rotation, Scale, spriteEffect, + decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(body.DrawPosition.X + transformedOffset.X, -(body.DrawPosition.Y + transformedOffset.Y)), c, + -body.Rotation + rotation, decorativeSprite.Scale * Scale, spriteEffect, depth: decorativeSprite.Sprite.Depth); } float depthStep = 0.000001f; diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index fd4394742..393c1228d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -5,13 +5,14 @@ using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Text; using System.Xml.Linq; using System.Globalization; using FarseerPhysics; using Barotrauma.Extensions; +using Barotrauma.Steam; namespace Barotrauma { @@ -217,6 +218,8 @@ namespace Barotrauma case "toggleupperhud": case "togglecharacternames": case "fpscounter": + case "dumptofile": + case "findentityids": return true; default: return client.HasConsoleCommandPermission(command); @@ -453,17 +456,10 @@ namespace Barotrauma GameMain.CharacterEditorScreen.Select(); })); - commands.Add(new Command("money", "", args => + commands.Add(new Command("steamnetdebug", "steamnetdebug: Toggles Steamworks debug logging.", (string[] args) => { - if (args.Length == 0) { return; } - if (GameMain.GameSession.GameMode is CampaignMode campaign) - { - if (int.TryParse(args[0], out int money)) - { - campaign.Money += money; - } - } - }, isCheat: true)); + SteamManager.NetworkingDebugLog = !SteamManager.NetworkingDebugLog; + })); AssignRelayToServer("kick", false); AssignRelayToServer("kickid", false); @@ -476,6 +472,7 @@ namespace Barotrauma AssignRelayToServer("help", false); AssignRelayToServer("verboselogging", false); AssignRelayToServer("freecam", false); + AssignRelayToServer("steamnetdebug", false); #if DEBUG AssignRelayToServer("crash", false); AssignRelayToServer("simulatedlatency", false); @@ -495,6 +492,7 @@ namespace Barotrauma AssignRelayToServer("setpassword", true); commands.Add(new Command("traitorlist", "", (string[] args) => { })); AssignRelayToServer("traitorlist", true); + AssignRelayToServer("money", true); AssignOnExecute("control", (string[] args) => { @@ -515,13 +513,14 @@ namespace Barotrauma AssignOnExecute("explosion", (string[] args) => { Vector2 explosionPos = GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition); - float range = 500, force = 10, damage = 50, structureDamage = 10, empStrength = 0.0f; + float range = 500, force = 10, damage = 50, structureDamage = 10, itemDamage = 100, empStrength = 0.0f; if (args.Length > 0) float.TryParse(args[0], out range); if (args.Length > 1) float.TryParse(args[1], out force); if (args.Length > 2) float.TryParse(args[2], out damage); if (args.Length > 3) float.TryParse(args[3], out structureDamage); - if (args.Length > 4) float.TryParse(args[4], out empStrength); - new Explosion(range, force, damage, structureDamage, empStrength).Explode(explosionPos, null); + if (args.Length > 4) float.TryParse(args[4], out itemDamage); + if (args.Length > 5) float.TryParse(args[5], out empStrength); + new Explosion(range, force, damage, structureDamage, itemDamage, empStrength).Explode(explosionPos, null); }); AssignOnExecute("teleportcharacter|teleport", (string[] args) => @@ -836,7 +835,7 @@ namespace Barotrauma return; } - if (Submarine.MainSub.SaveAs(System.IO.Path.Combine(SubmarineInfo.SavePath, fileName + ".sub"))) + if (Submarine.MainSub.SaveAs(Barotrauma.IO.Path.Combine(SubmarineInfo.SavePath, fileName + ".sub"))) { NewMessage("Sub saved", Color.Green); } @@ -1028,8 +1027,7 @@ namespace Barotrauma foreach (var deconstructItem in itemPrefab.DeconstructItems) { - var targetItem = MapEntityPrefab.Find(null, deconstructItem.ItemIdentifier, showErrorMessages: false) as ItemPrefab; - if (targetItem == null) + if (!(MapEntityPrefab.Find(null, deconstructItem.ItemIdentifier, showErrorMessages: false) is ItemPrefab targetItem)) { ThrowError("Error in item \"" + itemPrefab.Name + "\" - could not find deconstruct item \"" + deconstructItem.ItemIdentifier + "\"!"); continue; @@ -1044,9 +1042,14 @@ namespace Barotrauma if (fabricationRecipe != null) { - if (!fabricationRecipe.RequiredItems.Any(r => r.ItemPrefab == targetItem)) + var ingredient = fabricationRecipe.RequiredItems.Find(r => r.ItemPrefab == targetItem); + if (ingredient == null) { - NewMessage("Deconstructing \"" + itemPrefab.Name + "\" produces \"" + deconstructItem.ItemIdentifier + "\", which isn't required in the fabrication recipe of the item.", Color.Orange); + NewMessage("Deconstructing \"" + itemPrefab.Name + "\" produces \"" + deconstructItem.ItemIdentifier + "\", which isn't required in the fabrication recipe of the item.", Color.Red); + } + else if (ingredient.UseCondition && ingredient.MinCondition < deconstructItem.OutCondition) + { + NewMessage($"Deconstructing \"{itemPrefab.Name}\" produces more \"{deconstructItem.ItemIdentifier}\", than what's required to fabricate the item (required: {ingredient.ItemPrefab.Name} {(int)(ingredient.MinCondition * 100)}%, output: {deconstructItem.ItemIdentifier} {(int)(deconstructItem.OutCondition * 100)}%)", Color.Red); } } } @@ -1430,7 +1433,7 @@ namespace Barotrauma element.Value = lines[i]; i++; } - doc.Save(destinationPath); + doc.SaveSafe(destinationPath); }, () => { @@ -1465,7 +1468,7 @@ namespace Barotrauma while ((!(nextNode is XElement) || nextNode == element) && nextNode != null) nextNode = nextNode.NextNode; destinationElement = nextNode as XElement; } - destinationDoc.Save(destinationPath); + destinationDoc.SaveSafe(destinationPath); }, () => { @@ -1720,69 +1723,69 @@ namespace Barotrauma GameMain.Config.SaveNewPlayerConfig(); - var saveFiles = System.IO.Directory.GetFiles(SaveUtil.SaveFolder); + var saveFiles = Barotrauma.IO.Directory.GetFiles(SaveUtil.SaveFolder); foreach (string saveFile in saveFiles) { - System.IO.File.Delete(saveFile); + Barotrauma.IO.File.Delete(saveFile); NewMessage("Deleted " + saveFile, Color.Green); } - if (System.IO.Directory.Exists(System.IO.Path.Combine(SaveUtil.SaveFolder, "temp"))) + if (Barotrauma.IO.Directory.Exists(Barotrauma.IO.Path.Combine(SaveUtil.SaveFolder, "temp"))) { - System.IO.Directory.Delete(System.IO.Path.Combine(SaveUtil.SaveFolder, "temp"), true); + Barotrauma.IO.Directory.Delete(Barotrauma.IO.Path.Combine(SaveUtil.SaveFolder, "temp"), true); NewMessage("Deleted temp save folder", Color.Green); } - if (System.IO.Directory.Exists(ServerLog.SavePath)) + if (Barotrauma.IO.Directory.Exists(ServerLog.SavePath)) { - var logFiles = System.IO.Directory.GetFiles(ServerLog.SavePath); + var logFiles = Barotrauma.IO.Directory.GetFiles(ServerLog.SavePath); foreach (string logFile in logFiles) { - System.IO.File.Delete(logFile); + Barotrauma.IO.File.Delete(logFile); NewMessage("Deleted " + logFile, Color.Green); } } - if (System.IO.File.Exists("filelist.xml")) + if (Barotrauma.IO.File.Exists("filelist.xml")) { - System.IO.File.Delete("filelist.xml"); + Barotrauma.IO.File.Delete("filelist.xml"); NewMessage("Deleted filelist", Color.Green); } - if (System.IO.File.Exists("Data/bannedplayers.txt")) + if (Barotrauma.IO.File.Exists("Data/bannedplayers.txt")) { - System.IO.File.Delete("Data/bannedplayers.txt"); + Barotrauma.IO.File.Delete("Data/bannedplayers.txt"); NewMessage("Deleted bannedplayers.txt", Color.Green); } - if (System.IO.File.Exists("Submarines/TutorialSub.sub")) + if (Barotrauma.IO.File.Exists("Submarines/TutorialSub.sub")) { - System.IO.File.Delete("Submarines/TutorialSub.sub"); + Barotrauma.IO.File.Delete("Submarines/TutorialSub.sub"); NewMessage("Deleted TutorialSub from the submarine folder", Color.Green); } - /*if (System.IO.File.Exists(GameServer.SettingsFile)) + /*if (Barotrauma.IO.File.Exists(GameServer.SettingsFile)) { - System.IO.File.Delete(GameServer.SettingsFile); + Barotrauma.IO.File.Delete(GameServer.SettingsFile); NewMessage("Deleted server settings", Color.Green); } - if (System.IO.File.Exists(GameServer.ClientPermissionsFile)) + if (Barotrauma.IO.File.Exists(GameServer.ClientPermissionsFile)) { - System.IO.File.Delete(GameServer.ClientPermissionsFile); + Barotrauma.IO.File.Delete(GameServer.ClientPermissionsFile); NewMessage("Deleted client permission file", Color.Green); }*/ - if (System.IO.File.Exists("crashreport.log")) + if (Barotrauma.IO.File.Exists("crashreport.log")) { - System.IO.File.Delete("crashreport.log"); + Barotrauma.IO.File.Delete("crashreport.log"); NewMessage("Deleted crashreport.log", Color.Green); } - if (!System.IO.File.Exists("Content/Map/TutorialSub.sub")) + if (!Barotrauma.IO.File.Exists("Content/Map/TutorialSub.sub")) { ThrowError("TutorialSub.sub not found!"); } @@ -2196,7 +2199,7 @@ namespace Barotrauma ThrowError("Cannot use the flipx command while playing online."); return; } - Submarine.MainSub?.FlipX(); + if (Submarine.MainSub.SubBody != null) { Submarine.MainSub?.FlipX(); } }, isCheat: true)); commands.Add(new Command("gender", "Set the gender of the controlled character. Allowed parameters: Male, Female, None.", args => @@ -2318,9 +2321,16 @@ namespace Barotrauma } try { - SubmarineInfo subInfo = new SubmarineInfo(args[0]); - Submarine spawnedSub = Submarine.Load(subInfo, false); - spawnedSub.SetPosition(GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition)); + var subInfo = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.DisplayName.Equals(args[0], StringComparison.OrdinalIgnoreCase)); + if (subInfo == null) + { + ThrowError($"Could not find a submarine with the name \"{args[0]}\"."); + } + else + { + Submarine spawnedSub = Submarine.Load(subInfo, false); + spawnedSub.SetPosition(GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition)); + } } catch (Exception e) { @@ -2328,7 +2338,15 @@ namespace Barotrauma ThrowError(errorMsg, e); GameAnalyticsManager.AddErrorEventOnce("DebugConsole.SpawnSubmarine:Error", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg + '\n' + e.Message + '\n' + e.StackTrace); } - }, isCheat: true)); + }, + () => + { + return new string[][] + { + SubmarineInfo.SavedSubmarines.Select(s => s.DisplayName).ToArray() + }; + }, + isCheat: true)); commands.Add(new Command("pause", "Toggles the pause state when playing offline", (string[] args) => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs index 8dd4e5862..8755b9d40 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs @@ -26,7 +26,19 @@ namespace Barotrauma } } - item.body.FarseerBody.BodyType = BodyType.Kinematic; + int executedEffectCount = msg.ReadByte(); + for (int i = 0; i < executedEffectCount; i++) + { + int index1 = msg.ReadByte(); + int index2 = msg.ReadByte(); + var selectedEffect = statusEffects[index1][index2]; + item.ApplyStatusEffect(selectedEffect, selectedEffect.type, deltaTime: 1.0f, worldPosition: item.Position); + } + + if (item.body != null) + { + item.body.FarseerBody.BodyType = BodyType.Kinematic; + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs index 74a57080f..c16ab84b2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs @@ -7,76 +7,6 @@ using System.Xml.Linq; namespace Barotrauma { - public class ColorData - { - public int StartIndex, EndIndex; - public Color Color; - - private const char colorDefinitionIndicator = '‖'; - private const char lineChangeIndicator = '\n'; - private const string colorDefinitionStartString = "‖color:"; - private const string coloringEndDefinition = "‖color:end‖"; - - public static List GetColorData(string text, out string sanitizedText) - { - List textColors = null; - if (text != null && text.IndexOf(colorDefinitionIndicator) != -1 && text.Contains(colorDefinitionStartString)) - { - textColors = new List(); - List lineChangeIndexes = null; - - int currentIndex = text.IndexOf(lineChangeIndicator); - if (currentIndex != -1) - { - lineChangeIndexes = new List(); - lineChangeIndexes.Add(currentIndex); - int startIndex = currentIndex + 1; - - while (true) - { - if (startIndex >= text.Length) break; - currentIndex = text.IndexOf(lineChangeIndicator, startIndex); - if (currentIndex == -1) break; - lineChangeIndexes.Add(currentIndex); - startIndex = currentIndex + 1; - } - } - - while (text.IndexOf(colorDefinitionStartString) != -1) - { - ColorData colorData = new ColorData(); - - int colorDefinitionStartIndex = text.IndexOf(colorDefinitionStartString); - int colorDefinitionEndIndex = text.IndexOf(colorDefinitionIndicator, colorDefinitionStartIndex + 1); - - string[] colorDefinition = text.Substring(colorDefinitionStartIndex + colorDefinitionStartString.Length, colorDefinitionEndIndex - colorDefinitionStartIndex - colorDefinitionStartString.Length).Split(','); - - colorData.StartIndex = colorDefinitionStartIndex; - colorData.Color = new Color(int.Parse(colorDefinition[0]), int.Parse(colorDefinition[1]), int.Parse(colorDefinition[2])); - text = text.Remove(colorDefinitionStartIndex, colorDefinitionEndIndex - colorDefinitionStartIndex + 1); - colorData.EndIndex = text.IndexOf(coloringEndDefinition); - text = text.Remove(colorData.EndIndex, coloringEndDefinition.Length); - - if (lineChangeIndexes != null) - { - for (int i = 0; i < lineChangeIndexes.Count; i++) - { - if (colorData.StartIndex > lineChangeIndexes[i]) - { - colorData.StartIndex--; - colorData.EndIndex--; - } - } - } - - textColors.Add(colorData); - } - } - - sanitizedText = text; - return textColors; - } - } public class ScalableFont : IDisposable { private static List FontList = new List(); @@ -492,12 +422,12 @@ namespace Barotrauma } } - public void DrawStringWithColors(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects se, float layerDepth, List colorData) + public void DrawStringWithColors(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects se, float layerDepth, List richTextData) { - DrawStringWithColors(sb, text, position, color, rotation, origin, new Vector2(scale), se, layerDepth, colorData); + DrawStringWithColors(sb, text, position, color, rotation, origin, new Vector2(scale), se, layerDepth, richTextData); } - public void DrawStringWithColors(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth, List colorData) + public void DrawStringWithColors(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth, List richTextData) { if (textures.Count == 0 && !DynamicLoading) { return; } @@ -505,8 +435,8 @@ namespace Barotrauma Vector2 currentPos = position; Vector2 advanceUnit = rotation == 0.0f ? Vector2.UnitX : new Vector2((float)Math.Cos(rotation), (float)Math.Sin(rotation)); - int colorDataIndex = 0; - ColorData currentColorData = colorData[colorDataIndex]; + int richTextDataIndex = 0; + RichTextData currentRichTextData = richTextData[richTextDataIndex]; for (int i = 0; i < text.Length; i++) { @@ -527,15 +457,19 @@ namespace Barotrauma Color currentTextColor; - if (currentColorData != null && i > currentColorData.EndIndex + lineNum) + if (currentRichTextData != null && i > currentRichTextData.EndIndex + lineNum) { - colorDataIndex++; - currentColorData = colorDataIndex < colorData.Count ? colorData[colorDataIndex] : null; + richTextDataIndex++; + currentRichTextData = richTextDataIndex < richTextData.Count ? richTextData[richTextDataIndex] : null; } - if (currentColorData != null && currentColorData.StartIndex + lineNum <= i && i <= currentColorData.EndIndex + lineNum) + if (currentRichTextData != null && currentRichTextData.StartIndex + lineNum <= i && i <= currentRichTextData.EndIndex + lineNum) { - currentTextColor = currentColorData.Color; + currentTextColor = currentRichTextData.Color ?? color; + if (!string.IsNullOrEmpty(currentRichTextData.Metadata)) + { + currentTextColor = Color.Lerp(currentTextColor, Color.White, 0.5f); + } } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs index 39494a661..6724f3ab0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs @@ -52,7 +52,20 @@ namespace Barotrauma public GUITextBox InputBox { get; private set; } - public GUIButton ToggleButton; + private GUIButton toggleButton; + + public GUIButton ToggleButton + { + get => toggleButton; + set + { + if (toggleButton != null) + { + toggleButton.RectTransform.Parent = null; + } + toggleButton = value; + } + } private GUIButton showNewMessagesButton; @@ -109,7 +122,7 @@ namespace Barotrauma }; chatSendButton.RectTransform.AbsoluteOffset = new Point((int)(InputBox.Rect.Height * 0.15f), 0); InputBox.TextBlock.RectTransform.MaxSize - = new Point((int)(InputBox.Rect.Width - chatSendButton.Rect.Width * 1.25f - InputBox.TextBlock.Padding.Z), int.MaxValue); + = new Point((int)(InputBox.Rect.Width - chatSendButton.Rect.Width * 1.25f - InputBox.TextBlock.Padding.X - chatSendButton.RectTransform.AbsoluteOffset.X), int.MaxValue); showNewMessagesButton = new GUIButton(new RectTransform(new Vector2(1f, 0.075f), GUIFrame.RectTransform, Anchor.BottomCenter) { RelativeOffset = new Vector2(0.0f, 0.125f) }, TextManager.Get("chat.shownewmessages")); showNewMessagesButton.OnClicked += (GUIButton btn, object userdata) => @@ -190,22 +203,49 @@ 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 = new GUITextBlock(new RectTransform(new Vector2(0.98f, 0.0f), msgHolder.RectTransform) { AbsoluteOffset = new Point((int)(5 * GUI.Scale), 0) }, + GUITextBlock senderNameTimestamp = 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)) { - 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) + var senderNameBlock = new GUIButton(new RectTransform(new Vector2(0.8f, 1.0f), senderNameTimestamp.RectTransform) { AbsoluteOffset = new Point((int)(senderNameTimestamp.TextSize.X), 0) }, + senderName, textAlignment: Alignment.TopLeft, style: null, color: Color.Transparent) { - CanBeFocused = true + TextBlock = + { + Padding = Vector4.Zero + }, + Font = GUI.SmallFont, + CanBeFocused = true, + ForceUpperCase = false, + UserData = message.SenderClient, + OnClicked = (_, o) => + { + if (!(o is Client client)) { return false; } + GameMain.NetLobbyScreen?.SelectPlayer(client); + return true; + }, + OnSecondaryClicked = (_, o) => + { + if (!(o is Client client)) { return false; } + GameMain.GameSession?.CrewManager?.CreateModerationContextMenu(PlayerInput.MousePosition.ToPoint(), client); + return true; + }, + Text = senderName }; + + senderNameBlock.RectTransform.NonScaledSize = senderNameBlock.TextBlock.TextSize.ToPoint(); + senderNameBlock.TextBlock.OverrideTextColor(senderColor); + if (senderNameBlock.UserData != null) + { + senderNameBlock.TextBlock.HoverTextColor = Color.White; + } } 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) }, + { AbsoluteOffset = new Point((int)(10 * GUI.Scale), senderNameTimestamp == null ? 0 : senderNameTimestamp.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) { @@ -230,7 +270,7 @@ namespace Barotrauma 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); + senderNameTimestamp.RectTransform.MaxSize = new Point(msgHolder.Rect.Width - senderNameTimestamp.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; @@ -247,6 +287,11 @@ namespace Barotrauma showNewMessagesButton.Visible = true; } + if (message.Type == ChatMessageType.Server && message.ChangeType != PlayerConnectionChangeType.None) + { + TabMenu.StorePlayerConnectionChangeMessage(message); + } + if (!ToggleOpen) { var popupMsg = new GUIFrame(new RectTransform(Vector2.One, GUIFrame.RectTransform), style: "GUIToolTip") @@ -339,17 +384,6 @@ namespace Barotrauma prevUIScale = GUI.Scale; } - //hide chatbox when accessing the inventory of another character to prevent overlaps - if (Character.Controlled?.SelectedCharacter?.Inventory != null && - Character.Controlled.SelectedCharacter.CanInventoryBeAccessed) - { - SetVisibility(false); - } - else - { - SetVisibility(true); - } - if (showNewMessagesButton.Visible && chatBox.ScrollBar.BarScroll == 1f) { showNewMessagesButton.Visible = false; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs index b813565e0..0afc20985 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs @@ -1,7 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Text; @@ -38,7 +38,7 @@ namespace Barotrauma private static GUIDropDown fileTypeDropdown; private static GUIButton openButton; - private static FileSystemWatcher fileSystemWatcher; + private static System.IO.FileSystemWatcher fileSystemWatcher; private static string currentFileTypePattern; @@ -78,10 +78,10 @@ namespace Barotrauma currentDirectory += "/"; } fileSystemWatcher?.Dispose(); - fileSystemWatcher = new FileSystemWatcher(currentDirectory) + fileSystemWatcher = new System.IO.FileSystemWatcher(currentDirectory) { Filter = "*", - NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName + NotifyFilter = System.IO.NotifyFilters.LastWrite | System.IO.NotifyFilters.FileName | System.IO.NotifyFilters.DirectoryName }; fileSystemWatcher.Created += OnFileSystemChanges; fileSystemWatcher.Deleted += OnFileSystemChanges; @@ -97,11 +97,11 @@ namespace Barotrauma set; } - private static void OnFileSystemChanges(object sender, FileSystemEventArgs e) + private static void OnFileSystemChanges(object sender, System.IO.FileSystemEventArgs e) { switch (e.ChangeType) { - case WatcherChangeTypes.Created: + case System.IO.WatcherChangeTypes.Created: { var itemFrame = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), fileList.Content.RectTransform), e.Name) { @@ -114,15 +114,15 @@ namespace Barotrauma fileList.Content.RectTransform.SortChildren(SortFiles); } break; - case WatcherChangeTypes.Deleted: + case System.IO.WatcherChangeTypes.Deleted: { var itemFrame = fileList.Content.FindChild(c => (c is GUITextBlock tb) && (tb.Text == e.Name || tb.Text == e.Name + "/")); if (itemFrame != null) { fileList.RemoveChild(itemFrame); } } break; - case WatcherChangeTypes.Renamed: + case System.IO.WatcherChangeTypes.Renamed: { - RenamedEventArgs renameArgs = e as RenamedEventArgs; + System.IO.RenamedEventArgs renameArgs = e as System.IO.RenamedEventArgs; var itemFrame = fileList.Content.FindChild(c => (c is GUITextBlock tb) && (tb.Text == renameArgs.OldName || tb.Text == renameArgs.OldName + "/")) as GUITextBlock; itemFrame.UserData = (bool?)Directory.Exists(e.FullPath); itemFrame.Text = renameArgs.Name; @@ -156,7 +156,7 @@ namespace Barotrauma public static void Init() { - backgroundFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), style: null) + backgroundFrame = new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas), style: null) { Color = Color.Black * 0.5f, HoverColor = Color.Black * 0.5f, @@ -169,10 +169,10 @@ namespace Barotrauma var horizontalLayout = new GUILayoutGroup(new RectTransform(Vector2.One * 0.9f, window.RectTransform, Anchor.Center), true); sidebar = new GUIListBox(new RectTransform(new Vector2(0.29f, 1.0f), horizontalLayout.RectTransform)); - var drives = DriveInfo.GetDrives(); + var drives = System.IO.DriveInfo.GetDrives(); foreach (var drive in drives) { - if (drive.DriveType == DriveType.Ram) { continue; } + if (drive.DriveType == System.IO.DriveType.Ram) { continue; } if (ignoredDrivePrefixes.Any(p => drive.Name.StartsWith(p))) { continue; } new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), sidebar.Content.RectTransform), drive.Name.Replace('\\','/')); } @@ -348,17 +348,24 @@ namespace Barotrauma } IEnumerable files = null; - foreach (string pattern in currentFileTypePattern.Split(',')) + if (currentFileTypePattern == null) { - string patternTrimmed = pattern.Trim(); - patternTrimmed = "*" + filterBox.Text + "*" + patternTrimmed; - if (files == null) + files = Directory.GetFiles(currentDirectory); + } + else + { + foreach (string pattern in currentFileTypePattern.Split(',')) { - files = Directory.EnumerateFiles(currentDirectory, patternTrimmed); - } - else - { - files = files.Concat(Directory.EnumerateFiles(currentDirectory, patternTrimmed)); + string patternTrimmed = pattern.Trim(); + patternTrimmed = "*" + filterBox.Text + "*" + patternTrimmed; + if (files == null) + { + files = Directory.EnumerateFiles(currentDirectory, patternTrimmed); + } + else + { + files = files.Concat(Directory.EnumerateFiles(currentDirectory, patternTrimmed)); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index 5e3cba69a..f2d904791 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; using Barotrauma.CharacterEditor; @@ -80,13 +80,33 @@ namespace Barotrauma public static readonly string[] colorComponentLabels = { "R", "G", "B", "A" }; public static Vector2 ReferenceResolution => new Vector2(1920f, 1080f); - public static float Scale => (GameMain.GraphicsWidth / ReferenceResolution.X + GameMain.GraphicsHeight / ReferenceResolution.Y) / 2.0f * GameSettings.HUDScale; - public static float xScale => GameMain.GraphicsWidth / ReferenceResolution.X * GameSettings.HUDScale; + public static float Scale => (UIWidth / ReferenceResolution.X + GameMain.GraphicsHeight / ReferenceResolution.Y) / 2.0f * GameSettings.HUDScale; + public static float xScale => UIWidth / ReferenceResolution.X * GameSettings.HUDScale; public static float yScale => GameMain.GraphicsHeight / ReferenceResolution.Y * GameSettings.HUDScale; + public static int IntScale(float f) => (int)(f * Scale); + public static int IntScaleFloor(float f) => (int)Math.Floor(f * Scale); + public static int IntScaleCeiling(float f) => (int) Math.Ceiling(f * Scale); public static float HorizontalAspectRatio => GameMain.GraphicsWidth / (float)GameMain.GraphicsHeight; public static float VerticalAspectRatio => GameMain.GraphicsHeight / (float)GameMain.GraphicsWidth; public static float RelativeHorizontalAspectRatio => HorizontalAspectRatio / (ReferenceResolution.X / ReferenceResolution.Y); public static float RelativeVerticalAspectRatio => VerticalAspectRatio / (ReferenceResolution.Y / ReferenceResolution.X); + public static bool IsUltrawide => HorizontalAspectRatio > 2.0f; + + public static int UIWidth + { + get + { + // Ultrawide + if (IsUltrawide) + { + return (int)(GameMain.GraphicsHeight * ReferenceResolution.X / ReferenceResolution.Y); + } + else + { + return GameMain.GraphicsWidth; + } + } + } public static float SlicedSpriteScale { @@ -496,9 +516,58 @@ namespace Barotrauma if (MouseOn != null) { - DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - 500, 20), - $"Selected UI Element: {MouseOn.GetType().Name} ({ (MouseOn.Style?.Element.Name.LocalName ?? "no style") }, {MouseOn.Rect})", - Color.LightGreen, Color.Black * 0.5f, 0, SmallFont); + RectTransform mouseOnRect = MouseOn.RectTransform; + bool isAbsoluteOffsetInUse = mouseOnRect.AbsoluteOffset != Point.Zero || mouseOnRect.RelativeOffset == Vector2.Zero; + + string selectedString = $"Selected UI Element: {MouseOn.GetType().Name} ({ MouseOn.Style?.Element.Name.LocalName ?? "no style" }, {MouseOn.Rect}"; + string offsetString = $"Relative Offset: {mouseOnRect.RelativeOffset} | Absolute Offset: {(isAbsoluteOffsetInUse ? mouseOnRect.AbsoluteOffset : mouseOnRect.ParentRect.MultiplySize(mouseOnRect.RelativeOffset))}{(isAbsoluteOffsetInUse ? "" : " (Calculated from RelativeOffset)")}"; + string anchorPivotString = $"Anchor: {mouseOnRect.Anchor} | Pivot: {mouseOnRect.Pivot}"; + Vector2 selectedStringSize = SmallFont.MeasureString(selectedString); + Vector2 offsetStringSize = SmallFont.MeasureString(offsetString); + Vector2 anchorPivotStringSize = SmallFont.MeasureString(anchorPivotString); + + int padding = IntScale(10); + int yPos = padding; + + DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)selectedStringSize.X - padding, yPos), selectedString, Color.LightGreen, Color.Black, 0, SmallFont); + yPos += (int)selectedStringSize.Y + padding / 2; + + DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)offsetStringSize.X - padding, yPos), offsetString, Color.LightGreen, Color.Black, 0, SmallFont); + yPos += (int)offsetStringSize.Y + padding / 2; + + DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)anchorPivotStringSize.X - padding, yPos), anchorPivotString, Color.LightGreen, Color.Black, 0, SmallFont); + yPos += (int)anchorPivotStringSize.Y + padding / 2; + } + else + { + string guiScaleString = $"GUI.Scale: {Scale}"; + string guixScaleString = $"GUI.xScale: {xScale}"; + string guiyScaleString = $"GUI.yScale: {yScale}"; + string relativeHorizontalAspectRatioString = $"RelativeHorizontalAspectRatio: {RelativeHorizontalAspectRatio}"; + string relativeVerticalAspectRatioString = $"RelativeVerticalAspectRatio: {RelativeVerticalAspectRatio}"; + Vector2 guiScaleStringSize = SmallFont.MeasureString(guiScaleString); + Vector2 guixScaleStringSize = SmallFont.MeasureString(guixScaleString); + Vector2 guiyScaleStringSize = SmallFont.MeasureString(guiyScaleString); + Vector2 relativeHorizontalAspectRatioStringSize = SmallFont.MeasureString(relativeHorizontalAspectRatioString); + Vector2 relativeVerticalAspectRatioStringSize = SmallFont.MeasureString(relativeVerticalAspectRatioString); + + int padding = IntScale(10); + int yPos = padding; + + DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)guiScaleStringSize.X - padding, yPos), guiScaleString, Color.LightGreen, Color.Black, 0, SmallFont); + yPos += (int)guiScaleStringSize.Y + padding / 2; + + DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)guixScaleStringSize.X - padding, yPos), guixScaleString, Color.LightGreen, Color.Black, 0, SmallFont); + yPos += (int)guixScaleStringSize.Y + padding / 2; + + DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)guiyScaleStringSize.X - padding, yPos), guiyScaleString, Color.LightGreen, Color.Black, 0, SmallFont); + yPos += (int)guiyScaleStringSize.Y + padding / 2; + + DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)relativeHorizontalAspectRatioStringSize.X - padding, yPos), relativeHorizontalAspectRatioString, Color.LightGreen, Color.Black, 0, SmallFont); + yPos += (int)relativeHorizontalAspectRatioStringSize.Y + padding / 2; + + DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)relativeVerticalAspectRatioStringSize.X - padding, yPos), relativeVerticalAspectRatioString, Color.LightGreen, Color.Black, 0, SmallFont); + yPos += (int)relativeVerticalAspectRatioStringSize.Y + padding / 2; } } @@ -521,6 +590,34 @@ namespace Barotrauma MouseOn.DrawToolTip(spriteBatch); } + if (SubEditorScreen.IsSubEditor()) + { + // Draw our "infinite stack" on the cursor + switch (SubEditorScreen.DraggedItemPrefab) + { + case ItemPrefab itemPrefab: + { + var sprite = itemPrefab.InventoryIcon ?? itemPrefab.sprite; + sprite?.Draw(spriteBatch, PlayerInput.MousePosition, scale: Math.Min(64 / sprite.size.X, 64 / sprite.size.Y) * Scale); + break; + } + case ItemAssemblyPrefab iPrefab: + { + var (x, y) = PlayerInput.MousePosition; + foreach (var pair in iPrefab.DisplayEntities) + { + Rectangle dRect = pair.Second; + dRect = new Rectangle(x: (int)(dRect.X * iPrefab.Scale + x), + y: (int)(dRect.Y * iPrefab.Scale - y), + width: (int)(dRect.Width * iPrefab.Scale), + height: (int)(dRect.Height * iPrefab.Scale)); + pair.First.DrawPlacing(spriteBatch, dRect, pair.First.Scale * iPrefab.Scale); + } + break; + } + } + } + if (GameMain.WindowActive && !HideCursor) { spriteBatch.End(); @@ -541,6 +638,10 @@ namespace Barotrauma GameMain.GameScreen.PostProcessEffect.Parameters["blurDistance"].SetValue(0.001f * aberrationStrength); GameMain.GameScreen.PostProcessEffect.Parameters["chromaticAberrationStrength"].SetValue(new Vector3(-0.025f, -0.01f, -0.05f) * (float)(PerlinNoise.CalculatePerlin(aberrationT, aberrationT, 0) + 0.5f) * aberrationStrength); + + Matrix.CreateOrthographicOffCenter(0, GameMain.GraphicsWidth, GameMain.GraphicsHeight, 0, 0, -1, out Matrix projection); + + GameMain.GameScreen.PostProcessEffect.Parameters["MatrixTransform"].SetValue(projection); GameMain.GameScreen.PostProcessEffect.CurrentTechnique = GameMain.GameScreen.PostProcessEffect.Techniques["BlurChromaticAberration"]; GameMain.GameScreen.PostProcessEffect.CurrentTechnique.Passes[0].Apply(); @@ -780,6 +881,8 @@ namespace Barotrauma if (MouseCursor == CursorState.Waiting) { return CursorState.Waiting; } if (GUIScrollBar.DraggingBar != null) { return GUIScrollBar.DraggingBar.Bar.HoverCursor; } + if (SubEditorScreen.IsSubEditor() && SubEditorScreen.DraggedItemPrefab != null) { return CursorState.Hand; } + // Wire cursors if (Character.Controlled != null) { @@ -816,8 +919,7 @@ namespace Barotrauma case SubEditorScreen editor: { // Portrait area - if ((editor.CharacterMode || editor.WiringMode) && - HUDLayoutSettings.BottomRightInfoArea.Contains(PlayerInput.MousePosition)) + if (editor.WiringMode && HUDLayoutSettings.BottomRightInfoArea.Contains(PlayerInput.MousePosition)) { return CursorState.Hand; } @@ -1137,7 +1239,7 @@ namespace Barotrauma font.DrawString(sb, text, pos, color); } - public static void DrawStringWithColors(SpriteBatch sb, Vector2 pos, string text, Color color, List colorData, Color? backgroundColor = null, int backgroundPadding = 0, ScalableFont font = null, float depth = 0.0f) + public static void DrawStringWithColors(SpriteBatch sb, Vector2 pos, string text, Color color, List richTextData, Color? backgroundColor = null, int backgroundPadding = 0, ScalableFont font = null, float depth = 0.0f) { if (font == null) font = Font; if (backgroundColor != null) @@ -1146,7 +1248,7 @@ namespace Barotrauma DrawRectangle(sb, pos - Vector2.One * backgroundPadding, textSize + Vector2.One * 2.0f * backgroundPadding, (Color)backgroundColor, true, depth, 5); } - font.DrawStringWithColors(sb, text, pos, color, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, depth, colorData); + font.DrawStringWithColors(sb, text, pos, color, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, depth, richTextData); } public static void DrawRectangle(SpriteBatch sb, Vector2 start, Vector2 size, Color clr, bool isFilled = false, float depth = 0.0f, int thickness = 1) @@ -1863,8 +1965,9 @@ namespace Barotrauma Inventory.draggingItem = null; Inventory.DraggingInventory = null; - PauseMenu = new GUIFrame(new RectTransform(Vector2.One, Canvas), style: null, color: Color.Black * 0.5f); - + PauseMenu = new GUIFrame(new RectTransform(Vector2.One, Canvas, Anchor.Center), style: null); + new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, PauseMenu.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); + var pauseMenuInner = new GUIFrame(new RectTransform(new Vector2(0.13f, 0.3f), PauseMenu.RectTransform, Anchor.Center) { MinSize = new Point(250, 300) }); var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 0.6f), pauseMenuInner.RectTransform, Anchor.Center)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs index 2ad741c20..8595c032e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUICanvas.cs @@ -8,7 +8,7 @@ namespace Barotrauma { public class GUICanvas : RectTransform { - protected GUICanvas() : base(Vector2.One, parent: null) { } + protected GUICanvas() : base(size, parent: null) { } private static GUICanvas _instance; public static GUICanvas Instance @@ -22,15 +22,63 @@ namespace Barotrauma { GameMain.Instance.OnResolutionChanged += RecalculateSize; } + _instance.ItemComponentHolder = new GUIFrame(new RectTransform(Vector2.One, _instance, Anchor.Center)).RectTransform; } return _instance; } } + public RectTransform ItemComponentHolder; + + private static Vector2 size => new Vector2(GameMain.GraphicsWidth / (float)GUI.UIWidth, 1f); + + protected override Rectangle NonScaledUIRect => UIRect; + + private enum ResizeAxis { Both = 0, X = 1, Y = 2 } + // Turn public, if there is a need to call this manually. private static void RecalculateSize() { - Instance.Resize(Vector2.One, resizeChildren: true); + Vector2 recalculatedSize = size; + + // Scale children that are supposed to encompass the whole screen so that they are properly scaled on ultrawide as well + for (int i = 0; i < Instance.Children.Count(); i++) + { + RectTransform target = Instance.GetChild(i); + if (target == null || target.RelativeSize.X < 1 && target.RelativeSize.Y < 1) continue; + + ResizeAxis axis; + + if (target.RelativeSize.X >= 1 && target.RelativeSize.Y >= 1) + { + axis = ResizeAxis.Both; + } + else if (target.RelativeSize.X >= 1) + { + axis = ResizeAxis.X; + } + else + { + axis = ResizeAxis.Y; + } + + switch (axis) + { + case ResizeAxis.Both: + target.RelativeSize = recalculatedSize; + break; + + case ResizeAxis.X: + target.RelativeSize = new Vector2(recalculatedSize.X, target.RelativeSize.Y); + break; + + case ResizeAxis.Y: + target.RelativeSize = new Vector2(target.RelativeSize.X, recalculatedSize.Y); + break; + } + } + + Instance.Resize(size, resizeChildren: true); Instance.GetAllChildren().Select(c => c.GUIComponent as GUITextBlock).ForEach(t => t?.SetTextPos()); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index d733439b0..76f412e71 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -5,7 +5,7 @@ using System.Linq; using Barotrauma.Extensions; using System; using System.Xml.Linq; -using System.IO; +using Barotrauma.IO; using RestSharp; using System.Net; @@ -18,6 +18,9 @@ namespace Barotrauma public CursorState HoverCursor = CursorState.Default; + public delegate bool SecondaryButtonDownHandler(GUIComponent component, object userData); + public SecondaryButtonDownHandler OnSecondaryClicked; + public IEnumerable Children => RectTransform.Children.Select(c => c.GUIComponent); public T GetChild() where T : GUIComponent @@ -202,12 +205,12 @@ namespace Barotrauma set { RawToolTip = value; - TooltipColorData = ColorData.GetColorData(value, out value); + TooltipRichTextData = RichTextData.GetRichTextData(value, out value); toolTip = value; } } - public List TooltipColorData = null; + public List TooltipRichTextData = null; public GUIComponentStyle Style { @@ -451,6 +454,15 @@ namespace Barotrauma protected virtual void Update(float deltaTime) { if (!Visible) return; + + if (CanBeFocused && OnSecondaryClicked != null) + { + if (GUI.IsMouseOn(this) && PlayerInput.SecondaryMouseButtonClicked()) + { + OnSecondaryClicked?.Invoke(this, userData); + } + } + if (flashTimer > 0.0f) { flashTimer -= deltaTime; @@ -638,10 +650,10 @@ namespace Barotrauma public void DrawToolTip(SpriteBatch spriteBatch) { if (!Visible) return; - DrawToolTip(spriteBatch, ToolTip, GUI.MouseOn.Rect, TooltipColorData); + DrawToolTip(spriteBatch, ToolTip, GUI.MouseOn.Rect, TooltipRichTextData); } - public static void DrawToolTip(SpriteBatch spriteBatch, string toolTip, Rectangle targetElement, List colorData = null) + public static void DrawToolTip(SpriteBatch spriteBatch, string toolTip, Rectangle targetElement, List richTextData = null) { if (Tutorials.Tutorial.ContentRunning) { return; } @@ -651,7 +663,7 @@ namespace Barotrauma if (toolTipBlock == null || (string)toolTipBlock.userData != toolTip) { - toolTipBlock = new GUITextBlock(new RectTransform(new Point(width, height), null), colorData, toolTip, font: GUI.SmallFont, wrap: true, style: "GUIToolTip"); + toolTipBlock = new GUITextBlock(new RectTransform(new Point(width, height), null), richTextData, toolTip, font: GUI.SmallFont, wrap: true, style: "GUIToolTip"); toolTipBlock.RectTransform.NonScaledSize = new Point( (int)(GUI.SmallFont.MeasureString(toolTipBlock.WrappedText).X + padding.X + toolTipBlock.Padding.X + toolTipBlock.Padding.Z), (int)(GUI.SmallFont.MeasureString(toolTipBlock.WrappedText).Y + padding.Y + toolTipBlock.Padding.Y + toolTipBlock.Padding.W)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs index f4b41a3e8..d1560e7ed 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIDropDown.cs @@ -394,7 +394,7 @@ namespace Barotrauma if (Dropped) { - listBox.AddToGUIUpdateList(false, UpdateOrder); + listBox.AddToGUIUpdateList(false, 1); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs index 9e950e020..e8a601c8d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs @@ -1,12 +1,27 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; +using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; namespace Barotrauma { public class GUIImage : GUIComponent { + //paths of the textures that are being loaded asynchronously + private static readonly List activeTextureLoads = new List(); + + private static bool loadingTextures; + + public static bool LoadingTextures + { + get + { + return loadingTextures; + } + } + public float Rotation; private Sprite sprite; @@ -15,8 +30,12 @@ namespace Barotrauma private bool crop; - private bool scaleToFit; - + private readonly bool scaleToFit; + + private bool lazyLoaded, loading; + + public bool LoadAsynchronously; + public bool Crop { get @@ -75,7 +94,6 @@ namespace Barotrauma private GUIImage(RectTransform rectT, Sprite sprite, Rectangle? sourceRect, bool scaleToFit, string style) : base(style, rectT) { this.scaleToFit = scaleToFit; - sprite?.EnsureLazyLoaded(); Sprite = sprite; if (sourceRect.HasValue) { @@ -95,18 +113,45 @@ namespace Barotrauma } else { - rectT.SizeChanged += RecalculateScale; + if (Sprite != null && !Sprite.LazyLoad) + { + rectT.SizeChanged += RecalculateScale; + } } Enabled = true; } protected override void Draw(SpriteBatch spriteBatch) { - if (!Visible) return; + if (!Visible || loading) { return; } if (Parent != null) { State = Parent.State; } if (OverrideState != null) { State = OverrideState.Value; } + if (Sprite != null && Sprite.LazyLoad && !lazyLoaded) + { + if (LoadAsynchronously) + { + loadingTextures = true; + loading = true; + TaskPool.Add(LoadTextureAsync(), (Task) => + { + loading = false; + lazyLoaded = true; + RectTransform.SizeChanged += RecalculateScale; + RecalculateScale(); + }); + return; + } + else + { + Sprite.EnsureLazyLoaded(); + RectTransform.SizeChanged += RecalculateScale; + RecalculateScale(); + lazyLoaded = true; + } + } + Color currentColor = GetColor(State); if (BlendState != null) @@ -146,9 +191,53 @@ namespace Barotrauma private void RecalculateScale() { + if (sourceRect == Rectangle.Empty && sprite != null) + { + sourceRect = sprite.SourceRect; + } + Scale = sprite == null || sprite.SourceRect.Width == 0 || sprite.SourceRect.Height == 0 ? 1.0f : Math.Min(RectTransform.Rect.Width / (float)sprite.SourceRect.Width, RectTransform.Rect.Height / (float)sprite.SourceRect.Height); } + + private async Task LoadTextureAsync() + { + await Task.Yield(); + bool wait = true; + { + //if another GUIImage is already loading the same texture, wait for it to finish + while (wait) + { + await Task.Delay(5); + lock (activeTextureLoads) + { + wait = activeTextureLoads.Contains(Sprite.FullPath); + } + } + } + try + { + lock (activeTextureLoads) + { + activeTextureLoads.Add(Sprite.FullPath); + } + Sprite.EnsureLazyLoaded(); + } + finally + { + DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 10); + while (!Sprite.Loaded && DateTime.Now < timeOut) + { + await Task.Delay(5); + } + lock (activeTextureLoads) + { + activeTextureLoads.Remove(Sprite.FullPath); + loadingTextures = activeTextureLoads.Count > 0; + } + } + return true; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs index 7658be341..e2a351874 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs @@ -77,7 +77,6 @@ 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; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs index 013c7513b..3852ecca1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs @@ -70,6 +70,11 @@ namespace Barotrauma scrollBarNeedsRecalculation = true; } } + + /// + /// true if mouse down should select elements instead of mouse up + /// + private bool useMouseDownToSelect = false; private Vector4? overridePadding; public Vector4 Padding @@ -80,7 +85,11 @@ namespace Barotrauma if (Style == null) { return Vector4.Zero; } return Style.Padding; } - set { overridePadding = value; } + set + { + dimensionsNeedsRecalculation = true; + overridePadding = value; + } } public GUIComponent SelectedComponent @@ -182,10 +191,11 @@ namespace Barotrauma public GUIComponent DraggedElement => draggedElement; /// For horizontal listbox, default side is on the bottom. For vertical, it's on the right. - public GUIListBox(RectTransform rectT, bool isHorizontal = false, Color? color = null, string style = "", bool isScrollBarOnDefaultSide = true) : base(style, rectT) + public GUIListBox(RectTransform rectT, bool isHorizontal = false, Color? color = null, string style = "", bool isScrollBarOnDefaultSide = true, bool useMouseDownToSelect = false) : base(style, rectT) { CanBeFocused = true; selected = new List(); + this.useMouseDownToSelect = useMouseDownToSelect; ContentBackground = new GUIFrame(new RectTransform(Vector2.One, rectT), style) { CanBeFocused = false @@ -237,7 +247,7 @@ namespace Barotrauma UpdateDimensions(); } - private void UpdateDimensions() + public void UpdateDimensions() { dimensionsNeedsRecalculation = false; ContentBackground.RectTransform.Resize(Rect.Size); @@ -403,7 +413,10 @@ namespace Barotrauma if (Enabled && CanBeFocused && child.CanBeFocused && (GUI.IsMouseOn(child)) && child.Rect.Contains(PlayerInput.MousePosition)) { child.State = ComponentState.Hover; - if (PlayerInput.PrimaryMouseButtonClicked()) + + var mouseDown = useMouseDownToSelect ? PlayerInput.PrimaryMouseButtonDown() : PlayerInput.PrimaryMouseButtonClicked(); + + if (mouseDown) { Select(i, autoScroll: false); } @@ -426,7 +439,7 @@ namespace Barotrauma } else { - child.State = ComponentState.None; + child.State = !child.ExternalHighlight ? ComponentState.None : ComponentState.Hover; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs index 9eb0be459..7a48e553c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs @@ -11,7 +11,7 @@ namespace Barotrauma public static List MessageBoxes = new List(); private static int DefaultWidth { - get { return Math.Max(400, 400 * (GameMain.GraphicsWidth / 1920)); } + get { return Math.Max(400, (int)(400 * (GameMain.GraphicsWidth / GUI.ReferenceResolution.X))); } } private float inGameCloseTimer = 0.0f; @@ -63,7 +63,7 @@ namespace Barotrauma } public GUIMessageBox(string headerText, string text, string[] buttons, Vector2? relativeSize = null, Point? minSize = null, Alignment textAlignment = Alignment.TopLeft, Type type = Type.Default, string tag = "", Sprite icon = null) - : base(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: GUI.Style.GetComponentStyle("GUIMessageBox." + type) != null ? "GUIMessageBox." + type : "GUIMessageBox") + : base(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas, Anchor.Center), style: GUI.Style.GetComponentStyle("GUIMessageBox." + type) != null ? "GUIMessageBox." + type : "GUIMessageBox") { int width = (int)(DefaultWidth * (type == Type.Default ? 1.0f : 1.5f)), height = 0; if (relativeSize.HasValue) @@ -249,7 +249,10 @@ namespace Barotrauma InnerFrame.RectTransform.AbsoluteOffset = Vector2.SmoothStep(initialPos, defaultPos, openState).ToPoint(); openState = Math.Min(openState + deltaTime * 2.0f, 1.0f); - inGameCloseTimer += deltaTime; + if (GUI.MouseOn != InnerFrame && !InnerFrame.IsParentOf(GUI.MouseOn)) + { + inGameCloseTimer += deltaTime; + } if (inGameCloseTimer >= inGameCloseTime) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs index 01e36a545..75ddeeb79 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs @@ -59,6 +59,11 @@ namespace Barotrauma /// public Color Blue { get; private set; } = Color.Blue; + /// + /// General yellow color used for elements whose colors are set from code + /// + public Color Yellow { get; private set; } = Color.Yellow; + public Color ColorInventoryEmpty { get; private set; } = Color.Red; public Color ColorInventoryHalf { get; private set; } = Color.Orange; public Color ColorInventoryFull { get; private set; } = Color.LightGreen; @@ -128,6 +133,9 @@ namespace Barotrauma case "blue": Blue = subElement.GetAttributeColor("color", Blue); break; + case "yellow": + Yellow = subElement.GetAttributeColor("color", Yellow); + break; case "colorinventoryempty": ColorInventoryEmpty = subElement.GetAttributeColor("color", ColorInventoryEmpty); break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs index 73daf81f0..0725d9dcd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs @@ -238,9 +238,40 @@ namespace Barotrauma get { return censoredText; } } - private List colorData = null; - private bool hasColorHighlight = false; - + public class StrikethroughSettings + { + private Color color = GUI.Style.Red; + private int thickness; + private int expand; + + public StrikethroughSettings(Color? color = null, int thickness = 1, int expand = 0) + { + if (color != null) this.color = color.Value; + this.thickness = thickness; + this.expand = expand; + } + + public void Draw(SpriteBatch spriteBatch, float textSizeHalf, float xPos, float yPos) + { + ShapeExtensions.DrawLine(spriteBatch, new Vector2(xPos - textSizeHalf - expand, yPos), new Vector2(xPos + textSizeHalf + expand, yPos), color, thickness); + } + } + + public StrikethroughSettings Strikethrough = null; + + private readonly List richTextData = null; + + private readonly bool hasColorHighlight = false; + + public struct ClickableArea + { + public RichTextData Data; + + public delegate void OnClickDelegate(GUITextBlock textBlock, ClickableArea area); + public OnClickDelegate OnClick; + } + public List ClickableAreas { get; private set; } = new List(); + /// /// This is the new constructor. /// If the rectT height is set 0, the height is calculated from the text. @@ -282,11 +313,11 @@ namespace Barotrauma Enabled = true; Censor = false; } - public GUITextBlock(RectTransform rectT, List colorData, string text, Color? textColor = null, ScalableFont font = null, Alignment textAlignment = Alignment.Left, bool wrap = false, string style = "", Color? color = null, bool playerInput = false) + public GUITextBlock(RectTransform rectT, List richTextData, string text, Color? textColor = null, ScalableFont font = null, Alignment textAlignment = Alignment.Left, bool wrap = false, string style = "", Color? color = null, bool playerInput = false) : this(rectT, text, textColor, font, textAlignment, wrap, style, color, playerInput) { - this.colorData = colorData; - hasColorHighlight = colorData != null; + this.richTextData = richTextData; + hasColorHighlight = richTextData != null; } public void CalculateHeightFromText(int padding = 0) @@ -427,6 +458,131 @@ namespace Barotrauma disabledTextColor = color; } + protected List> GetAllPositions() + { + float halfHeight = Font.MeasureString("T").Y * 0.5f; + string textDrawn = Censor ? CensoredText : WrappedText; + var positions = new List>(); + if (textDrawn.Contains("\n")) + { + string[] lines = textDrawn.Split('\n'); + int index = 0; + int totalIndex = 0; + for (int i = 0; i < lines.Length; i++) + { + string line = lines[i]; + totalIndex += line.Length; + float totalTextHeight = Font.MeasureString(textDrawn.Substring(0, totalIndex)).Y; + for (int j = 0; j <= line.Length; j++) + { + Vector2 lineTextSize = Font.MeasureString(line.Substring(0, j)); + Vector2 indexPos = new Vector2(lineTextSize.X + Padding.X, totalTextHeight + Padding.Y - halfHeight); + //DebugConsole.NewMessage($"index: {index}, pos: {indexPos}", Color.AliceBlue); + positions.Add(new Tuple(indexPos, index + j)); + } + index = totalIndex; + } + } + else + { + textDrawn = Censor ? CensoredText : Text; + for (int i = 0; i <= Text.Length; i++) + { + Vector2 textSize = Font.MeasureString(textDrawn.Substring(0, i)); + Vector2 indexPos = new Vector2(textSize.X + Padding.X, textSize.Y + Padding.Y - halfHeight) + TextPos - Origin; + //DebugConsole.NewMessage($"index: {i}, pos: {indexPos}", Color.WhiteSmoke); + positions.Add(new Tuple(indexPos, i)); + } + } + return positions; + } + + public int GetCaretIndexFromScreenPos(Vector2 pos) + { + return GetCaretIndexFromLocalPos(pos - Rect.Location.ToVector2()); + } + + public int GetCaretIndexFromLocalPos(Vector2 pos) + { + var positions = GetAllPositions(); + if (positions.Count == 0) { return 0; } + float halfHeight = Font.MeasureString("T").Y * 0.5f; + + var currPosition = positions[0]; + + float topY = positions.Min(p => p.Item1.Y); + + for (int i = 1; i < positions.Count; i++) + { + var p1 = positions[i]; + var p2 = currPosition; + + float diffY = Math.Abs(p1.Item1.Y - pos.Y) - Math.Abs(p2.Item1.Y - pos.Y); + if (diffY < -3.0f) + { + currPosition = p1; continue; + } + else if (diffY > 3.0f) + { + continue; + } + else + { + diffY = Math.Abs(p1.Item1.Y - pos.Y); + if (diffY < halfHeight || (p1.Item1.Y == topY && pos.Y < topY)) + { + //we are on this line, select the nearest character + float diffX = Math.Abs(p1.Item1.X - pos.X) - Math.Abs(p2.Item1.X - pos.X); + if (diffX < -1.0f) + { + currPosition = p1; continue; + } + else + { + continue; + } + } + else + { + //we are on a different line, preserve order + if (p1.Item2 < p2.Item2) + { + if (p1.Item1.Y > pos.Y) { currPosition = p1; } + } + else if (p1.Item2 > p2.Item2) + { + if (p1.Item1.Y < pos.Y) { currPosition = p1; } + } + continue; + } + } + } + //GUI.AddMessage($"index: {posIndex.Item2}, pos: {posIndex.Item1}", Color.WhiteSmoke); + return currPosition != null ? currPosition.Item2 : Text.Length; + } + + protected override void Update(float deltaTime) + { + base.Update(deltaTime); + + if (ClickableAreas.Any() && (GUI.MouseOn?.IsParentOf(this) ?? true) && Rect.Contains(PlayerInput.MousePosition)) + { + int index = GetCaretIndexFromScreenPos(PlayerInput.MousePosition); + foreach (ClickableArea clickableArea in ClickableAreas) + { + if (clickableArea.Data.StartIndex <= index && index <= clickableArea.Data.EndIndex) + { + GUI.MouseCursor = CursorState.Hand; + if (PlayerInput.PrimaryMouseButtonClicked()) + { + clickableArea.OnClick?.Invoke(this, clickableArea); + } + break; + } + } + } + } + protected override void Draw(SpriteBatch spriteBatch) { if (!Visible) { return; } @@ -480,7 +636,12 @@ namespace Barotrauma else { Font.DrawStringWithColors(spriteBatch, Censor ? censoredText : (Wrap ? wrappedText : text), pos, - currentTextColor * (currentTextColor.A / 255.0f), 0.0f, origin, TextScale, SpriteEffects.None, textDepth, colorData); + currentTextColor * (currentTextColor.A / 255.0f), 0.0f, origin, TextScale, SpriteEffects.None, textDepth, richTextData); + } + + if (Strikethrough != null) + { + Strikethrough.Draw(spriteBatch, (int)Math.Ceiling(TextSize.X / 2f), pos.X, ForceUpperCase ? pos.Y : pos.Y + GUI.Scale * 2f); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index 2b67a0674..9be942058 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -67,7 +67,12 @@ namespace Barotrauma private Vector2 selectionEndPos; private Vector2 selectionRectSize; + private bool mouseHeldInside; + private readonly Memento memento = new Memento(); + + // Skip one update cycle, fixes Enter key instantly deselecting the chatbox + private bool skipUpdate; public GUIFrame Frame { @@ -362,114 +367,14 @@ namespace Barotrauma caretPosDirty = false; } - protected List> GetAllPositions() - { - float halfHeight = Font.MeasureString("T").Y * 0.5f; - string textDrawn = Censor ? textBlock.CensoredText : textBlock.WrappedText; - var positions = new List>(); - if (textDrawn.Contains("\n")) - { - string[] lines = textDrawn.Split('\n'); - int index = 0; - int totalIndex = 0; - for (int i = 0; i < lines.Length; i++) - { - string line = lines[i]; - totalIndex += line.Length; - float totalTextHeight = Font.MeasureString(textDrawn.Substring(0, totalIndex)).Y; - for (int j = 0; j <= line.Length; j++) - { - Vector2 lineTextSize = Font.MeasureString(line.Substring(0, j)); - Vector2 indexPos = new Vector2(lineTextSize.X + textBlock.Padding.X, totalTextHeight + textBlock.Padding.Y - halfHeight); - //DebugConsole.NewMessage($"index: {index}, pos: {indexPos}", Color.AliceBlue); - positions.Add(new Tuple(indexPos, index + j)); - } - index = totalIndex; - } - } - else - { - textDrawn = Censor ? textBlock.CensoredText : textBlock.Text; - for (int i = 0; i <= textBlock.Text.Length; i++) - { - Vector2 textSize = Font.MeasureString(textDrawn.Substring(0, i)); - Vector2 indexPos = new Vector2(textSize.X + textBlock.Padding.X, textSize.Y + textBlock.Padding.Y - halfHeight) + textBlock.TextPos - textBlock.Origin; - //DebugConsole.NewMessage($"index: {i}, pos: {indexPos}", Color.WhiteSmoke); - positions.Add(new Tuple(indexPos, i)); - } - } - return positions; - } - - public int GetCaretIndexFromScreenPos(Vector2 pos) - { - return GetCaretIndexFromLocalPos(pos - textBlock.Rect.Location.ToVector2()); - } - - public int GetCaretIndexFromLocalPos(Vector2 pos) - { - var positions = GetAllPositions(); - if (positions.Count==0) { return 0; } - float halfHeight = Font.MeasureString("T").Y * 0.5f; - - var currPosition = positions[0]; - - for (int i=1;i 3.0f) - { - continue; - } - else - { - diffY = Math.Abs(p1.Item1.Y - pos.Y); - if (diffY < halfHeight) - { - //we are on this line, select the nearest character - float diffX = Math.Abs(p1.Item1.X - pos.X) - Math.Abs(p2.Item1.X - pos.X); - if (diffX < -1.0f) - { - currPosition = p1; continue; - } - else - { - continue; - } - } - else - { - //we are on a different line, preserve order - if (p1.Item2 < p2.Item2) - { - if (p1.Item1.Y > pos.Y) { currPosition = p1; } - } - else if (p1.Item2 > p2.Item2) - { - if (p1.Item1.Y < pos.Y) { currPosition = p1; } - } - continue; - } - } - } - //GUI.AddMessage($"index: {posIndex.Item2}, pos: {posIndex.Item1}", Color.WhiteSmoke); - return currPosition != null ? currPosition.Item2 : textBlock.Text.Length; - } - public void Select(int forcedCaretIndex = -1) { + skipUpdate = true; if (memento.Current == null) { memento.Store(Text); } - CaretIndex = forcedCaretIndex == - 1 ? GetCaretIndexFromScreenPos(PlayerInput.MousePosition) : forcedCaretIndex; + CaretIndex = forcedCaretIndex == - 1 ? textBlock.GetCaretIndexFromScreenPos(PlayerInput.MousePosition) : forcedCaretIndex; ClearSelection(); selected = true; GUI.KeyboardDispatcher.Subscriber = this; @@ -499,11 +404,19 @@ namespace Barotrauma if (flashTimer > 0.0f) flashTimer -= deltaTime; if (!Enabled) { return; } + + if (skipUpdate) + { + skipUpdate = false; + return; + } + if (MouseRect.Contains(PlayerInput.MousePosition) && (GUI.MouseOn == null || (!(GUI.MouseOn is GUIButton) && GUI.IsMouseOn(this)))) { State = ComponentState.Hover; if (PlayerInput.PrimaryMouseButtonDown()) { + mouseHeldInside = true; Select(); } else @@ -518,7 +431,7 @@ namespace Barotrauma { if (!MathUtils.NearlyEqual(PlayerInput.MouseSpeed.X, 0)) { - CaretIndex = GetCaretIndexFromScreenPos(PlayerInput.MousePosition); + CaretIndex = textBlock.GetCaretIndexFromScreenPos(PlayerInput.MousePosition); CalculateCaretPos(); CalculateSelection(); } @@ -526,7 +439,11 @@ namespace Barotrauma } else { - if ((PlayerInput.LeftButtonClicked() || PlayerInput.RightButtonClicked()) && selected) Deselect(); + if ((PlayerInput.LeftButtonClicked() || PlayerInput.RightButtonClicked()) && selected) + { + if (!mouseHeldInside) { Deselect(); } + mouseHeldInside = false; + } isSelecting = false; State = ComponentState.None; } @@ -793,7 +710,7 @@ namespace Barotrauma InitSelectionStart(); } float lineHeight = Font.MeasureString("T").Y; - int newIndex = GetCaretIndexFromLocalPos(new Vector2(caretPos.X, caretPos.Y-lineHeight)); + int newIndex = textBlock.GetCaretIndexFromLocalPos(new Vector2(caretPos.X, caretPos.Y-lineHeight)); CaretIndex = newIndex; caretTimer = 0; HandleSelection(); @@ -804,7 +721,7 @@ namespace Barotrauma InitSelectionStart(); } lineHeight = Font.MeasureString("T").Y; - newIndex = GetCaretIndexFromLocalPos(new Vector2(caretPos.X, caretPos.Y+lineHeight)); + newIndex = textBlock.GetCaretIndexFromLocalPos(new Vector2(caretPos.X, caretPos.Y+lineHeight)); CaretIndex = newIndex; caretTimer = 0; HandleSelection(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs index 8df88614e..6be9c9283 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs @@ -50,10 +50,6 @@ namespace Barotrauma get; private set; } - /*public static Rectangle HealthBarAreaRight - { - get; private set; - }*/ public static Rectangle HealthBarArea { get; private set; @@ -120,17 +116,11 @@ namespace Barotrauma //horizontal slices at the corners of the screen for health bar and affliction icons int afflictionAreaHeight = (int)(50 * GUI.Scale); - int healthBarWidth = BottomRightInfoArea.Width + CharacterInventory.SlotSize.X + CharacterInventory.Spacing * 2 + CharacterInventory.HideButtonWidth; + int healthBarWidth = (int)(BottomRightInfoArea.Width * 1.58f); int healthBarHeight = (int)(50f * GUI.Scale); - HealthBarArea = new Rectangle(BottomRightInfoArea.X - (healthBarWidth - BottomRightInfoArea.Width) + (int)(2 * GUI.Scale), BottomRightInfoArea.Y - healthBarHeight + (int)(10 * GUI.Scale), healthBarWidth, healthBarHeight); - AfflictionAreaLeft = new Rectangle(HealthBarArea.X, HealthBarArea.Y - Padding - afflictionAreaHeight, HealthBarArea.Width, afflictionAreaHeight); - - //HealthBarAreaRight = new Rectangle(Padding, GameMain.GraphicsHeight - healthBarHeight - Padding, healthBarWidth, healthBarHeight); - /*if (HealthBarAreaRight.Y + healthBarHeight * 0.75f < PortraitArea.Y) - { - HealthBarAreaRight = new Rectangle(GameMain.GraphicsWidth - Padding - healthBarWidth, HealthBarAreaRight.Y, HealthBarAreaRight.Width, HealthBarAreaRight.Height); - }*/ - //AfflictionAreaRight = new Rectangle(HealthBarAreaRight.X, HealthBarAreaRight.Y + healthBarHeight + Padding, healthBarWidth, afflictionAreaHeight); + HealthBarArea = new Rectangle(BottomRightInfoArea.Right - healthBarWidth + (int)Math.Floor(1 / GUI.Scale), BottomRightInfoArea.Y - healthBarHeight + GUI.IntScale(10), healthBarWidth, healthBarHeight); + AfflictionAreaLeft = new Rectangle(HealthBarArea.X, HealthBarArea.Y - Padding - afflictionAreaHeight, HealthBarArea.Width, afflictionAreaHeight); + int messageAreaWidth = GameMain.GraphicsWidth / 3; MessageAreaTop = new Rectangle((GameMain.GraphicsWidth - messageAreaWidth) / 2, ButtonAreaTop.Bottom, messageAreaWidth, ButtonAreaTop.Height); @@ -146,7 +136,7 @@ namespace Barotrauma CrewArea = new Rectangle(Padding, Padding, (int)Math.Max(400 * GUI.Scale, 220), ObjectiveAnchor.Top - Padding * 2); - InventoryAreaLower = new Rectangle(Padding, inventoryTopY, GameMain.GraphicsWidth - Padding * 2, GameMain.GraphicsHeight - inventoryTopY); + InventoryAreaLower = new Rectangle(ChatBoxArea.Right + Padding * 7, inventoryTopY, GameMain.GraphicsWidth - Padding * 9 - ChatBoxArea.Width, GameMain.GraphicsHeight - inventoryTopY); int healthWindowWidth = (int)(GameMain.GraphicsWidth * 0.5f); int healthWindowHeight = (int)(GameMain.GraphicsWidth * 0.5f * 0.65f); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs index f730a55f2..79b77f528 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs @@ -338,6 +338,8 @@ namespace Barotrauma PendingSplashScreens.Clear(); currSplashScreen = null; } + + if (currSplashScreen == null) { return; } } if (currSplashScreen.IsPlaying) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs index d61cc17e9..8f7c9d2bf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs @@ -253,11 +253,12 @@ namespace Barotrauma return _rect; } } - public Rectangle ParentRect => Parent != null ? Parent.Rect : ScreenRect; - + public Rectangle ParentRect => Parent != null ? Parent.Rect : UIRect; protected Rectangle NonScaledRect => new Rectangle(NonScaledTopLeft, NonScaledSize); - protected Rectangle NonScaledParentRect => parent != null ? Parent.NonScaledRect : ScreenRect; - protected Rectangle ScreenRect => new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight); + protected virtual Rectangle NonScaledUIRect => NonScaledRect; + protected Rectangle NonScaledParentRect => parent != null ? Parent.NonScaledRect : UIRect; + protected Rectangle NonScaledParentUIRect => parent != null ? Parent.NonScaledUIRect : UIRect; + protected Rectangle UIRect => new Rectangle(0, 0, GUI.UIWidth, GameMain.GraphicsHeight); private Pivot pivot; /// @@ -444,14 +445,14 @@ namespace Barotrauma protected void RecalculateRelativeSize() { - relativeSize = new Vector2(NonScaledSize.X, NonScaledSize.Y) / new Vector2(NonScaledParentRect.Width, NonScaledParentRect.Height); + relativeSize = new Vector2(NonScaledSize.X, NonScaledSize.Y) / new Vector2(NonScaledParentUIRect.Width, NonScaledParentUIRect.Height); recalculateRect = true; SizeChanged?.Invoke(); } protected void RecalculateAbsoluteSize() { - Point size = NonScaledParentRect.Size; + Point size = NonScaledParentUIRect.Size; switch (ScaleBasis) { case ScaleBasis.BothWidth: @@ -651,6 +652,13 @@ namespace Barotrauma Parent.ChildrenChanged?.Invoke(this); } + public void ReverseChildren() + { + children.Reverse(); + RecalculateAll(false, false, true); + Parent.ChildrenChanged?.Invoke(this); + } + public void SetAsLastChild() { if (IsLastChild) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs new file mode 100644 index 000000000..deea61de3 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -0,0 +1,941 @@ +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System.Linq; +using Barotrauma.Networking; + +namespace Barotrauma +{ + class TabMenu + { + public static bool PendingChanges = false; + + private static bool initialized = false; + + private static UISprite spectateIcon, deadIcon, disconnectedIcon; + private static Sprite ownerIcon, moderatorIcon; + + private enum InfoFrameTab { Crew, Mission, MyCharacter, Traitor }; + private static InfoFrameTab selectedTab; + private GUIFrame infoFrame, contentFrame; + + private readonly List tabButtons = new List(); + private GUIFrame infoFrameHolder; + private List linkedGUIList; + private GUIListBox logList; + private GUIListBox[] crewListArray; + private float sizeMultiplier = 1f; + + private IEnumerable crew; + private List teamIDs; + private const string inLobbyString = "\u2022 \u2022 \u2022"; + + private static Color ownCharacterBGColor = Color.Gold * 0.7f; + + private class LinkedGUI + { + private const ushort lowPingThreshold = 100; + private const ushort mediumPingThreshold = 200; + + private ushort currentPing; + private Client client; + private Character character; + private bool hasCharacter; + private GUITextBlock textBlock; + private GUIFrame frame; + + public LinkedGUI(Client client, GUIFrame frame, bool hasCharacter, GUITextBlock textBlock) + { + this.client = client; + this.textBlock = textBlock; + this.frame = frame; + this.hasCharacter = hasCharacter; + } + + public LinkedGUI(Character character, GUIFrame frame, bool hasCharacter, GUITextBlock textBlock) + { + this.character = character; + this.textBlock = textBlock; + this.frame = frame; + this.hasCharacter = hasCharacter; + } + + public bool HasMultiplayerCharacterChanged() + { + if (client == null) return false; + bool characterState = client.Character != null; + if (characterState && client.Character.IsDead) characterState = false; + return hasCharacter != characterState; + } + + public bool HasMultiplayerCharacterDied() + { + if (client == null || !hasCharacter || client.Character == null) return false; + return client.Character.IsDead; + } + + public bool HasAICharacterDied() + { + if (character == null) return false; + return character.IsDead; + } + + public void TryPingRefresh() + { + if (client == null) return; + if (currentPing == client.Ping) return; + currentPing = client.Ping; + textBlock.Text = currentPing.ToString(); + textBlock.TextColor = GetPingColor(); + } + + private Color GetPingColor() + { + if (currentPing < lowPingThreshold) + { + return GUI.Style.Green; + } + else if (currentPing < mediumPingThreshold) + { + return GUI.Style.Yellow; + } + else + { + return GUI.Style.Red; + } + } + + public void Remove(GUIFrame parent) + { + parent.RemoveChild(frame); + } + } + + public void Initialize() + { + spectateIcon = GUI.Style.GetComponentStyle("SpectateIcon").Sprites[GUIComponent.ComponentState.None][0]; + deadIcon = GUI.Style.GetComponentStyle("DeadIcon").Sprites[GUIComponent.ComponentState.None][0]; + disconnectedIcon = GUI.Style.GetComponentStyle("DisconnectedIcon").Sprites[GUIComponent.ComponentState.None][0]; + ownerIcon = GUI.Style.GetComponentStyle("OwnerIcon").Sprites[GUIComponent.ComponentState.None][0].Sprite; + moderatorIcon = GUI.Style.GetComponentStyle("ModeratorIcon").Sprites[GUIComponent.ComponentState.None][0].Sprite; + initialized = true; + } + + public TabMenu() + { + if (!initialized) Initialize(); + + CreateInfoFrame(selectedTab); + SelectInfoFrameTab(null, selectedTab); + } + + public void Update() + { + if (selectedTab != InfoFrameTab.Crew) return; + if (linkedGUIList == null) return; + + if (GameMain.IsMultiplayer) + { + for (int i = 0; i < linkedGUIList.Count; i++) + { + linkedGUIList[i].TryPingRefresh(); + if (linkedGUIList[i].HasMultiplayerCharacterChanged() || linkedGUIList[i].HasMultiplayerCharacterDied() || linkedGUIList[i].HasAICharacterDied()) + { + RemoveCurrentElements(); + CreateMultiPlayerList(true); + return; + } + } + } + else + { + for (int i = 0; i < linkedGUIList.Count; i++) + { + if (linkedGUIList[i].HasAICharacterDied()) + { + RemoveCurrentElements(); + CreateSinglePlayerList(true); + } + } + } + } + + public void AddToGUIUpdateList() + { + infoFrame?.AddToGUIUpdateList(); + NetLobbyScreen.JobInfoFrame?.AddToGUIUpdateList(); + } + + public static void OnRoundEnded() + { + storedMessages.Clear(); + PendingChanges = false; + } + + private void CreateInfoFrame(InfoFrameTab selectedTab) + { + tabButtons.Clear(); + + infoFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null); + new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, infoFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); + + switch (selectedTab) + { + case InfoFrameTab.Crew: + case InfoFrameTab.Mission: + case InfoFrameTab.Traitor: + default: + contentFrame = new GUIFrame(new RectTransform(new Vector2(0.33f, 0.667f), infoFrame.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { /*MinSize = new Point(width, height),*/ RelativeOffset = new Vector2(0.025f, 0.12f) }); + break; + case InfoFrameTab.MyCharacter: + contentFrame = new GUIFrame(new RectTransform(new Vector2(0.33f, 0.5f), infoFrame.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { /*MinSize = new Point(width, height),*/ RelativeOffset = new Vector2(0.025f, 0.12f) }); + break; + } + + var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.958f, 0.943f), contentFrame.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { AbsoluteOffset = new Point(0, GUI.IntScale(17.5f)) }, style: null); + var buttonArea = new GUILayoutGroup(new RectTransform(new Point(innerFrame.Rect.Width, GUI.IntScale(25f)), innerFrame.RectTransform) { AbsoluteOffset = new Point(2, 0) }, isHorizontal: true) + { + RelativeSpacing = 0.01f + }; + + infoFrameHolder = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.926f), innerFrame.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter), style: null); + + var crewButton = new GUIButton(new RectTransform(new Vector2(0.245f, 1.0f), buttonArea.RectTransform), TextManager.Get("Crew"), style: "GUITabButton") + { + UserData = InfoFrameTab.Crew, + OnClicked = SelectInfoFrameTab + }; + tabButtons.Add(crewButton); + + var missionButton = new GUIButton(new RectTransform(new Vector2(0.245f, 1.0f), buttonArea.RectTransform), TextManager.Get("Mission"), style: "GUITabButton") + { + UserData = InfoFrameTab.Mission, + OnClicked = SelectInfoFrameTab + }; + tabButtons.Add(missionButton); + + bool isTraitor = GameMain.Client?.Character?.IsTraitor ?? false; + if (isTraitor && GameMain.Client.TraitorMission != null) + { + var traitorButton = new GUIButton(new RectTransform(new Vector2(0.245f, 1.0f), buttonArea.RectTransform), TextManager.Get("tabmenu.traitor"), style: "GUITabButton") + { + UserData = InfoFrameTab.Traitor, + OnClicked = SelectInfoFrameTab + }; + tabButtons.Add(traitorButton); + } + + if (GameMain.NetworkMember != null) + { + var myCharacterButton = new GUIButton(new RectTransform(new Vector2(0.245f, 1.0f), buttonArea.RectTransform), TextManager.Get("tabmenu.character"), style: "GUITabButton") + { + UserData = InfoFrameTab.MyCharacter, + OnClicked = SelectInfoFrameTab + }; + tabButtons.Add(myCharacterButton); + } + } + + private bool SelectInfoFrameTab(GUIButton button, object userData) + { + selectedTab = (InfoFrameTab)userData; + + CreateInfoFrame(selectedTab); + tabButtons.ForEach(tb => tb.Selected = (InfoFrameTab)tb.UserData == selectedTab); + + switch (selectedTab) + { + case InfoFrameTab.Crew: + CreateCrewListFrame(infoFrameHolder); + break; + case InfoFrameTab.Mission: + CreateMissionInfo(infoFrameHolder); + break; + case InfoFrameTab.Traitor: + TraitorMissionPrefab traitorMission = GameMain.Client.TraitorMission; + Character traitor = GameMain.Client.Character; + if (traitor == null || traitorMission == null) return false; + CreateTraitorInfo(infoFrameHolder, traitorMission, traitor); + break; + case InfoFrameTab.MyCharacter: + if (GameMain.NetworkMember == null) { return false; } + GameMain.NetLobbyScreen.CreatePlayerFrame(infoFrameHolder); + break; + } + + return true; + } + + private const float jobColumnWidthPercentage = 0.138f; + private const float characterColumnWidthPercentage = 0.656f; + private const float pingColumnWidthPercentage = 0.206f; + + private int jobColumnWidth, characterColumnWidth, pingColumnWidth; + + private void CreateCrewListFrame(GUIFrame crewFrame) + { + crew = GameMain.GameSession.CrewManager.GetCharacters(); + teamIDs = crew.Select(c => c.TeamID).Distinct().ToList(); + + // Show own team first when there's more than one team + if (teamIDs.Count > 1 && GameMain.Client.Character != null) + { + Character.TeamType ownTeam = GameMain.Client.Character.TeamID; + teamIDs = teamIDs.OrderBy(i => i != ownTeam).ThenBy(i => i).ToList(); + } + + if (!teamIDs.Any()) teamIDs.Add(Character.TeamType.None); + + var content = new GUILayoutGroup(new RectTransform(Vector2.One, crewFrame.RectTransform)); + + crewListArray = new GUIListBox[teamIDs.Count]; + GUILayoutGroup[] headerFrames = new GUILayoutGroup[teamIDs.Count]; + + float nameHeight = 0.075f; + + Vector2 crewListSize = new Vector2(1f, 1f / teamIDs.Count - (teamIDs.Count > 1 ? nameHeight * 1.1f : 0f)); + for (int i = 0; i < teamIDs.Count; i++) + { + if (teamIDs.Count > 1) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, nameHeight), content.RectTransform), CombatMission.GetTeamName(teamIDs[i]), textColor: i == 0 ? GUI.Style.Green : GUI.Style.Orange) { ForceUpperCase = true }; + } + + headerFrames[i] = new GUILayoutGroup(new RectTransform(Vector2.Zero, content.RectTransform, Anchor.TopLeft, Pivot.BottomLeft) { AbsoluteOffset = new Point(2, -1) }, isHorizontal: true) + { + AbsoluteSpacing = 2, + UserData = i + }; + + GUIListBox crewList = new GUIListBox(new RectTransform(crewListSize, content.RectTransform)) + { + Padding = new Vector4(2, 5, 0, 0), + AutoHideScrollBar = false + }; + crewList.UpdateDimensions(); + + if (teamIDs.Count > 1) + { + crewList.OnSelected = (component, obj) => + { + for (int i = 0; i < crewListArray.Length; i++) + { + if (crewListArray[i] == crewList) continue; + crewListArray[i].Deselect(); + } + SelectElement(component.UserData, crewList); + return true; + }; + } + else + { + crewList.OnSelected = (component, obj) => + { + SelectElement(component.UserData, crewList); + return true; + }; + } + + crewListArray[i] = crewList; + } + + for (int i = 0; i < teamIDs.Count; i++) + { + headerFrames[i].RectTransform.RelativeSize = new Vector2(1f - crewListArray[i].ScrollBar.Rect.Width / (float)crewListArray[i].Rect.Width, GUI.HotkeyFont.Size / (float)crewFrame.RectTransform.Rect.Height * 1.5f); + + if (!GameMain.IsMultiplayer) + { + CreateSinglePlayerListContentHolder(headerFrames[i]); + } + else + { + CreateMultiPlayerListContentHolder(headerFrames[i]); + } + } + + crewFrame.RectTransform.AbsoluteOffset = new Point(0, (int)(headerFrames[0].Rect.Height * headerFrames.Length) - (teamIDs.Count > 1 ? GUI.IntScale(10f) : 0)); + + if (GameMain.IsMultiplayer) + { + CreateMultiPlayerList(false); + CreateMultiPlayerLogContent(crewFrame); + } + else + { + CreateSinglePlayerList(false); + } + } + + private void CreateSinglePlayerListContentHolder(GUILayoutGroup headerFrame) + { + GUIButton jobButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("tabmenu.job"), style: "GUIButtonSmallFreeScale"); + GUIButton characterButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("name"), style: "GUIButtonSmallFreeScale"); + + sizeMultiplier = (headerFrame.Rect.Width - headerFrame.AbsoluteSpacing * (headerFrame.CountChildren - 1)) / (float)headerFrame.Rect.Width; + + jobButton.RectTransform.RelativeSize = new Vector2(jobColumnWidthPercentage * sizeMultiplier, 1f); + characterButton.RectTransform.RelativeSize = new Vector2((1f - jobColumnWidthPercentage * sizeMultiplier) * sizeMultiplier, 1f); + + jobButton.TextBlock.Font = characterButton.TextBlock.Font = GUI.HotkeyFont; + jobButton.CanBeFocused = characterButton.CanBeFocused = false; + jobButton.TextBlock.ForceUpperCase = characterButton.TextBlock.ForceUpperCase = true; + + jobColumnWidth = jobButton.Rect.Width; + characterColumnWidth = characterButton.Rect.Width; + } + + private void CreateSinglePlayerList(bool refresh) + { + if (refresh) + { + crew = GameMain.GameSession.CrewManager.GetCharacters(); + } + + linkedGUIList = new List(); + + for (int i = 0; i < teamIDs.Count; i++) + { + foreach (Character character in crew.Where(c => c.TeamID == teamIDs[i])) + { + CreateSinglePlayerCharacterElement(character, i); + } + } + } + + private void CreateSinglePlayerCharacterElement(Character character, int i) + { + GUIFrame frame = new GUIFrame(new RectTransform(new Point(crewListArray[i].Content.Rect.Width, GUI.IntScale(33f)), crewListArray[i].Content.RectTransform), style: "ListBoxElement") + { + UserData = character, + Color = (Character.Controlled == character) ? ownCharacterBGColor : Color.Transparent + }; + + var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: true) + { + AbsoluteSpacing = 2 + }; + + new GUICustomComponent(new RectTransform(new Point(jobColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), onDraw: (sb, component) => character.Info.DrawJobIcon(sb, component.Rect)) + { + CanBeFocused = false, + HoverColor = Color.White, + SelectedColor = Color.White + }; + + GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), + ToolBox.LimitString(character.Info.Name, GUI.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: character.Info.Job.Prefab.UIColor); + + linkedGUIList.Add(new LinkedGUI(character, frame, !character.IsDead, null)); + } + + private void CreateMultiPlayerListContentHolder(GUILayoutGroup headerFrame) + { + GUIButton jobButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("tabmenu.job"), style: "GUIButtonSmallFreeScale"); + GUIButton characterButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("name"), style: "GUIButtonSmallFreeScale"); + GUIButton pingButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("serverlistping"), style: "GUIButtonSmallFreeScale"); + + sizeMultiplier = (headerFrame.Rect.Width - headerFrame.AbsoluteSpacing * (headerFrame.CountChildren - 1)) / (float)headerFrame.Rect.Width; + + jobButton.RectTransform.RelativeSize = new Vector2(jobColumnWidthPercentage * sizeMultiplier, 1f); + characterButton.RectTransform.RelativeSize = new Vector2(characterColumnWidthPercentage * sizeMultiplier, 1f); + pingButton.RectTransform.RelativeSize = new Vector2(pingColumnWidthPercentage * sizeMultiplier, 1f); + + jobButton.TextBlock.Font = characterButton.TextBlock.Font = pingButton.TextBlock.Font = GUI.HotkeyFont; + jobButton.CanBeFocused = characterButton.CanBeFocused = pingButton.CanBeFocused = false; + jobButton.TextBlock.ForceUpperCase = characterButton.TextBlock.ForceUpperCase = pingButton.ForceUpperCase = true; + + jobColumnWidth = jobButton.Rect.Width; + characterColumnWidth = characterButton.Rect.Width; + pingColumnWidth = pingButton.Rect.Width; + } + + private void CreateMultiPlayerList(bool refresh) + { + if (refresh) + { + crew = GameMain.GameSession.CrewManager.GetCharacters(); + } + + linkedGUIList = new List(); + + List connectedClients = GameMain.Client.ConnectedClients; + + for (int i = 0; i < teamIDs.Count; i++) + { + foreach (Character character in crew.Where(c => c.TeamID == teamIDs[i])) + { + if (!(character is AICharacter) && connectedClients.Find(c => c.Character == null && c.Name == character.Name) != null) continue; + CreateMultiPlayerCharacterElement(character, GameMain.Client.ConnectedClients.Find(c => c.Character == character), i); + } + } + + for (int j = 0; j < connectedClients.Count; j++) + { + Client client = connectedClients[j]; + + if (!client.InGame || client.Character == null || client.Character.IsDead) + { + CreateMultiPlayerClientElement(client); + } + } + } + + private void CreateMultiPlayerCharacterElement(Character character, Client client, int i) + { + GUIFrame frame = new GUIFrame(new RectTransform(new Point(crewListArray[i].Content.Rect.Width, GUI.IntScale(33f)), crewListArray[i].Content.RectTransform), style: "ListBoxElement") + { + UserData = character, + Color = (GameMain.NetworkMember != null && GameMain.Client.Character == character) ? ownCharacterBGColor : Color.Transparent + }; + + frame.OnSecondaryClicked += (component, data) => + { + GameMain.GameSession?.CrewManager?.CreateModerationContextMenu(PlayerInput.MousePosition.ToPoint(), client); + return true; + }; + + var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: true) + { + AbsoluteSpacing = 2 + }; + + new GUICustomComponent(new RectTransform(new Point(jobColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), onDraw: (sb, component) => character.Info.DrawJobIcon(sb, component.Rect)) + { + CanBeFocused = false, + HoverColor = Color.White, + SelectedColor = Color.White + }; + + if (client != null) + { + CreateNameWithPermissionIcon(client, paddedFrame); + linkedGUIList.Add(new LinkedGUI(client, frame, true, new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), client.Ping.ToString(), textAlignment: Alignment.Center))); + } + else + { + GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), + ToolBox.LimitString(character.Info.Name, GUI.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: character.Info.Job.Prefab.UIColor); + + if (character is AICharacter) + { + linkedGUIList.Add(new LinkedGUI(character, frame, !character.IsDead, new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), TextManager.Get("tabmenu.bot"), textAlignment: Alignment.Center) { ForceUpperCase = true })); + } + else + { + linkedGUIList.Add(new LinkedGUI(client: null, frame, true, null)); + + new GUICustomComponent(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), onDraw: (sb, component) => DrawDisconnectedIcon(sb, component.Rect)) + { + CanBeFocused = false, + HoverColor = Color.White, + SelectedColor = Color.White + }; + } + } + } + + private void CreateMultiPlayerClientElement(Client client) + { + int teamIndex = GetTeamIndex(client); + if (teamIndex == -1) teamIndex = 0; + + GUIFrame frame = new GUIFrame(new RectTransform(new Point(crewListArray[teamIndex].Content.Rect.Width, GUI.IntScale(33f)), crewListArray[teamIndex].Content.RectTransform), style: "ListBoxElement") + { + UserData = client, + Color = Color.Transparent + }; + + var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: true) + { + AbsoluteSpacing = 2 + }; + + new GUICustomComponent(new RectTransform(new Point(jobColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), + onDraw: (sb, component) => DrawNotInGameIcon(sb, component.Rect, client)) + { + CanBeFocused = false, + HoverColor = Color.White, + SelectedColor = Color.White + }; + + CreateNameWithPermissionIcon(client, paddedFrame); + linkedGUIList.Add(new LinkedGUI(client, frame, false, new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), client.Ping.ToString(), textAlignment: Alignment.Center))); + } + + private int GetTeamIndex(Client client) + { + if (teamIDs.Count <= 1) return 0; + + if (client.Character != null) + { + return teamIDs.IndexOf(client.Character.TeamID); + } + + if (client.CharacterID != 0) + { + foreach (Character c in crew) + { + if (client.CharacterID == c.ID) + { + return teamIDs.IndexOf(c.TeamID); + } + } + } + else + { + foreach (Character c in crew) + { + if (client.Name == c.Name) + { + return teamIDs.IndexOf(c.TeamID); + } + } + } + + return 0; + } + + private void CreateNameWithPermissionIcon(Client client, GUILayoutGroup paddedFrame) + { + GUITextBlock characterNameBlock; + Sprite permissionIcon = GetPermissionIcon(client); + JobPrefab prefab = client.Character?.Info?.Job?.Prefab; + Color nameColor = prefab != null ? prefab.UIColor : Color.White; + + if (permissionIcon != null) + { + Point iconSize = permissionIcon.SourceRect.Size; + float characterNameWidthAdjustment = (iconSize.X + paddedFrame.AbsoluteSpacing) / characterColumnWidth; + + characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), + ToolBox.LimitString(client.Name, GUI.Font, (int)(characterColumnWidth - paddedFrame.Rect.Width * characterNameWidthAdjustment)), textAlignment: Alignment.Center, textColor: nameColor); + + float iconWidth = iconSize.X / (float)characterColumnWidth; + int xOffset = (int)(jobColumnWidth + characterNameBlock.TextPos.X - GUI.Font.MeasureString(characterNameBlock.Text).X / 2f - paddedFrame.AbsoluteSpacing - iconWidth * paddedFrame.Rect.Width); + new GUIImage(new RectTransform(new Vector2(iconWidth, 1f), paddedFrame.RectTransform) { AbsoluteOffset = new Point(xOffset + 2, 0) }, permissionIcon) { IgnoreLayoutGroups = true }; + } + else + { + characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), + ToolBox.LimitString(client.Name, GUI.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: nameColor); + } + + if (client.Character != null && client.Character.IsDead) + { + characterNameBlock.Strikethrough = new GUITextBlock.StrikethroughSettings(null, GUI.IntScale(1f), GUI.IntScale(5f)); + } + } + + private Sprite GetPermissionIcon(Client client) + { + if (GameMain.NetworkMember == null || client == null || !client.HasPermissions) return null; + + if (!client.AllowKicking) // Owner cannot be kicked + { + return ownerIcon; + } + else + { + return moderatorIcon; + } + } + + private void DrawNotInGameIcon(SpriteBatch spriteBatch, Rectangle area, Client client) + { + if (client.Spectating) + { + spectateIcon.Draw(spriteBatch, area, Color.White); + } + else if (client.Character != null && client.Character.IsDead) + { + client.Character.Info.DrawJobIcon(spriteBatch, area); + } + else + { + Vector2 stringOffset = GUI.GlobalFont.MeasureString(inLobbyString) / 2f; + GUI.GlobalFont.DrawString(spriteBatch, inLobbyString, area.Center.ToVector2() - stringOffset, Color.White); + } + } + + private void DrawDisconnectedIcon(SpriteBatch spriteBatch, Rectangle area) + { + disconnectedIcon.Draw(spriteBatch, area, GUI.Style.Red); + } + + /// + /// Select an element from CrewListFrame + /// + private bool SelectElement(object userData, GUIComponent crewList) + { + Character character = userData as Character; + Client client = userData as Client; + + GUIComponent existingPreview = infoFrameHolder.FindChild("SelectedCharacter"); + if (existingPreview != null) infoFrameHolder.RemoveChild(existingPreview); + + GUIFrame background = new GUIFrame(new RectTransform(new Vector2(0.543f, 0.717f), infoFrameHolder.RectTransform, Anchor.TopLeft, Pivot.TopRight) { RelativeOffset = new Vector2(-0.061f, 0) }) + { + UserData = "SelectedCharacter" + }; + + if (character != null) + { + if (GameMain.NetworkMember == null) + { + GUIComponent preview = character.Info.CreateInfoFrame(background, false, null); + } + else + { + GUIComponent preview = character.Info.CreateInfoFrame(background, false, GetPermissionIcon(GameMain.Client.ConnectedClients.Find(c => c.Character == character))); + GameMain.Client.SelectCrewCharacter(character, preview); + } + } + else if (client != null) + { + GUIComponent preview = CreateClientInfoFrame(background, client, GetPermissionIcon(client)); + if (GameMain.NetworkMember != null) GameMain.Client.SelectCrewClient(client, preview); + } + + return true; + } + + private GUIComponent CreateClientInfoFrame(GUIFrame frame, Client client, Sprite permissionIcon = null) + { + GUIComponent paddedFrame; + + if (client.Character == null) + { + paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.874f, 0.58f), frame.RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.05f) }) + { + RelativeSpacing = 0.05f + //Stretch = true + }; + + var headerArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.322f), paddedFrame.RectTransform), isHorizontal: true); + + new GUICustomComponent(new RectTransform(new Vector2(0.425f, 1.0f), headerArea.RectTransform), + onDraw: (sb, component) => DrawNotInGameIcon(sb, component.Rect, client)); + + ScalableFont font = paddedFrame.Rect.Width < 280 ? GUI.SmallFont : GUI.Font; + + var headerTextArea = new GUILayoutGroup(new RectTransform(new Vector2(0.575f, 1.0f), headerArea.RectTransform)) + { + RelativeSpacing = 0.02f, + Stretch = true + }; + + GUITextBlock clientNameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), ToolBox.LimitString(client.Name, GUI.Font, headerTextArea.Rect.Width), textColor: Color.White, font: GUI.Font) + { + ForceUpperCase = true, + Padding = Vector4.Zero + }; + + if (permissionIcon != null) + { + Point iconSize = permissionIcon.SourceRect.Size; + int iconWidth = (int)((float)clientNameBlock.Rect.Height / iconSize.Y * iconSize.X); + new GUIImage(new RectTransform(new Point(iconWidth, clientNameBlock.Rect.Height), clientNameBlock.RectTransform) { AbsoluteOffset = new Point(-iconWidth - 2, 0) }, permissionIcon) { IgnoreLayoutGroups = true }; + } + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), client.Spectating ? TextManager.Get("playingasspectator") : TextManager.Get("tabmenu.inlobby"), textColor: Color.White, font: font, wrap: true) + { + Padding = Vector4.Zero + }; + } + else + { + paddedFrame = client.Character.Info.CreateInfoFrame(frame, false, permissionIcon); + } + + return paddedFrame; + } + + private void CreateMultiPlayerLogContent(GUIFrame crewFrame) + { + var logContainer = new GUIFrame(new RectTransform(new Vector2(0.543f, 0.717f), crewFrame.RectTransform, Anchor.TopRight, Pivot.TopLeft) { RelativeOffset = new Vector2(-0.061f, 0) }); + var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.900f, 0.900f), logContainer.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { RelativeOffset = new Vector2(0f, 0.0475f) }, style: null); + var content = new GUILayoutGroup(new RectTransform(Vector2.One, innerFrame.RectTransform)) + { + Stretch = true + }; + + logList = new GUIListBox(new RectTransform(Vector2.One, content.RectTransform)) + { + Padding = new Vector4(0, 10 * GUI.Scale, 0, 10 * GUI.Scale), + UserData = crewFrame, + AutoHideScrollBar = false, + Spacing = (int)(5 * GUI.Scale) + }; + + foreach (Pair pair in storedMessages) + { + AddLineToLog(pair.First, pair.Second); + } + + logList.BarScroll = 1f; + } + + private static readonly List> storedMessages = new List>(); + + public static void StorePlayerConnectionChangeMessage(ChatMessage message) + { + if (!GameMain.GameSession?.GameMode?.IsRunning ?? true) { return; } + + string msg = ChatMessage.GetTimeStamp() + message.TextWithSender; + storedMessages.Add(new Pair(msg, message.ChangeType)); + + if (GameSession.IsTabMenuOpen) + { + TabMenu instance = GameSession.TabMenuInstance; + instance.AddLineToLog(msg, message.ChangeType); + + // Update crew + if (selectedTab == InfoFrameTab.Crew) + { + instance.RemoveCurrentElements(); + instance.CreateMultiPlayerList(true); + } + } + } + + private void RemoveCurrentElements() + { + for (int i = 0; i < crewListArray.Length; i++) + { + for (int j = 0; j < linkedGUIList.Count; j++) + { + linkedGUIList[j].Remove(crewListArray[i].Content); + } + } + + linkedGUIList.Clear(); + } + + private void AddLineToLog(string line, PlayerConnectionChangeType type) + { + Color textColor = Color.White; + + switch (type) + { + case PlayerConnectionChangeType.Joined: + textColor = GUI.Style.Green; + break; + case PlayerConnectionChangeType.Kicked: + textColor = GUI.Style.Orange; + break; + case PlayerConnectionChangeType.Disconnected: + textColor = GUI.Style.Yellow; + break; + case PlayerConnectionChangeType.Banned: + textColor = GUI.Style.Red; + break; + } + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), logList.Content.RectTransform), line, wrap: true, font: GUI.SmallFont) + { + TextColor = textColor, + CanBeFocused = false, + UserData = line + }.CalculateHeightFromText(); + + //if ((prevSize == 1.0f && listBox.BarScroll == 0.0f) || (prevSize < 1.0f && listBox.BarScroll == 1.0f)) listBox.BarScroll = 1.0f; + } + + private void CreateMissionInfo(GUIFrame infoFrame) + { + infoFrame.ClearChildren(); + GUIFrame missionFrame = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox"); + int padding = (int)(0.0245f * missionFrame.Rect.Height); + Location endLocation = GameMain.GameSession.EndLocation; + Sprite portrait = endLocation.Type.GetPortrait(endLocation.PortraitId); + bool hasPortrait = portrait != null && portrait.SourceRect.Width > 0 && portrait.SourceRect.Height > 0; + int contentWidth = hasPortrait ? (int)(missionFrame.Rect.Width * 0.951f) : missionFrame.Rect.Width - padding * 2; + + Vector2 locationNameSize = GUI.LargeFont.MeasureString(endLocation.Name); + Vector2 locationTypeSize = GUI.SubHeadingFont.MeasureString(endLocation.Name); + GUITextBlock locationNameText = new GUITextBlock(new RectTransform(new Point(contentWidth, (int)locationNameSize.Y), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, padding) }, endLocation.Name, font: GUI.LargeFont); + GUITextBlock locationTypeText = new GUITextBlock(new RectTransform(new Point(contentWidth, (int)locationTypeSize.Y), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, locationNameText.Rect.Height + padding) }, endLocation.Type.Name, font: GUI.SubHeadingFont); + + int locationInfoYOffset = locationNameText.Rect.Height + locationTypeText.Rect.Height + padding * 2; + + GUIFrame missionDescriptionHolder; + + if (hasPortrait) + { + GUIFrame missionImageHolder = new GUIFrame(new RectTransform(new Point(contentWidth, (int)(missionFrame.Rect.Height * 0.588f)), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, locationInfoYOffset) }); + float portraitAspectRatio = portrait.SourceRect.Width / portrait.SourceRect.Height; + GUIImage portraitImage = new GUIImage(new RectTransform(new Vector2(1.0f, 1f), missionImageHolder.RectTransform), portrait, scaleToFit: true); + missionImageHolder.RectTransform.NonScaledSize = new Point(portraitImage.Rect.Size.X, (int)(portraitImage.Rect.Size.X / portraitAspectRatio)); + missionDescriptionHolder = new GUIFrame(new RectTransform(new Point(contentWidth, 0), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, missionImageHolder.RectTransform.AbsoluteOffset.Y + missionImageHolder.Rect.Height + padding) }, style: null); + } + else + { + missionDescriptionHolder = new GUIFrame(new RectTransform(new Point(contentWidth, 0), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, locationInfoYOffset) }, style: null); + } + + Mission mission = GameMain.GameSession?.Mission; + if (mission != null) + { + GUILayoutGroup missionTextGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.744f, 0f), missionDescriptionHolder.RectTransform, Anchor.CenterLeft) { RelativeOffset = new Vector2(0.225f, 0f) }, false, childAnchor: Anchor.TopLeft); + + string missionNameString = ToolBox.WrapText(mission.Name, missionTextGroup.Rect.Width, GUI.LargeFont); + string missionDescriptionString = ToolBox.WrapText(mission.Description, missionTextGroup.Rect.Width, GUI.Font); + string missionRewardString = ToolBox.WrapText(TextManager.GetWithVariable("MissionReward", "[reward]", mission.Reward.ToString()), missionTextGroup.Rect.Width, GUI.Font); + + Vector2 missionNameSize = GUI.LargeFont.MeasureString(missionNameString); + Vector2 missionDescriptionSize = GUI.Font.MeasureString(missionDescriptionString); + Vector2 missionRewardSize = GUI.Font.MeasureString(missionRewardString); + + missionDescriptionHolder.RectTransform.NonScaledSize = new Point(missionDescriptionHolder.RectTransform.NonScaledSize.X, (int)(missionNameSize.Y + missionDescriptionSize.Y + missionRewardSize.Y)); + missionTextGroup.RectTransform.NonScaledSize = new Point(missionTextGroup.RectTransform.NonScaledSize.X, missionDescriptionHolder.RectTransform.NonScaledSize.Y); + + float iconAspectRatio = mission.Prefab.Icon.SourceRect.Width / mission.Prefab.Icon.SourceRect.Height; + int iconWidth = (int)(0.225f * missionDescriptionHolder.RectTransform.NonScaledSize.X); + int iconHeight = Math.Max(missionTextGroup.RectTransform.NonScaledSize.Y, (int)(iconWidth * iconAspectRatio)); + Point iconSize = new Point(iconWidth, iconHeight); + + new GUIImage(new RectTransform(iconSize, missionDescriptionHolder.RectTransform), mission.Prefab.Icon, null, true) { Color = mission.Prefab.IconColor }; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionNameString, font: GUI.LargeFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionRewardString); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionDescriptionString); + } + else + { + GUILayoutGroup missionTextGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0f), missionDescriptionHolder.RectTransform, Anchor.CenterLeft), false, childAnchor: Anchor.TopLeft); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), TextManager.Get("NoMission"), font: GUI.LargeFont); + } + } + + private void CreateTraitorInfo(GUIFrame infoFrame, TraitorMissionPrefab traitorMission, Character traitor) + { + GUIFrame missionFrame = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox"); + + int padding = (int)(0.0245f * missionFrame.Rect.Height); + + GUIFrame missionDescriptionHolder = new GUIFrame(new RectTransform(new Point(missionFrame.Rect.Width - padding * 2, 0), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, padding) }, style: null); + GUILayoutGroup missionTextGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.65f, 0f), missionDescriptionHolder.RectTransform, Anchor.CenterLeft) { RelativeOffset = new Vector2(0.319f, 0f) }, false, childAnchor: Anchor.TopLeft); + + string missionNameString = ToolBox.WrapText(TextManager.Get("tabmenu.traitor"), missionTextGroup.Rect.Width, GUI.LargeFont); + string missionDescriptionString = ToolBox.WrapText(traitor.TraitorCurrentObjective, missionTextGroup.Rect.Width, GUI.Font); + + Vector2 missionNameSize = GUI.LargeFont.MeasureString(missionNameString); + Vector2 missionDescriptionSize = GUI.Font.MeasureString(missionDescriptionString); + + missionDescriptionHolder.RectTransform.NonScaledSize = new Point(missionDescriptionHolder.RectTransform.NonScaledSize.X, (int)(missionNameSize.Y + missionDescriptionSize.Y)); + missionTextGroup.RectTransform.NonScaledSize = new Point(missionTextGroup.RectTransform.NonScaledSize.X, missionDescriptionHolder.RectTransform.NonScaledSize.Y); + + float aspectRatio = traitorMission.Icon.SourceRect.Width / traitorMission.Icon.SourceRect.Height; + + int iconWidth = (int)(0.319f * missionDescriptionHolder.RectTransform.NonScaledSize.X); + int iconHeight = Math.Max(missionTextGroup.RectTransform.NonScaledSize.Y, (int)(iconWidth * aspectRatio)); + Point iconSize = new Point(iconWidth, iconHeight); + + new GUIImage(new RectTransform(iconSize, missionDescriptionHolder.RectTransform), traitorMission.Icon, null, true) { Color = traitorMission.IconColor }; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionNameString, font: GUI.LargeFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionDescriptionString); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/VideoPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/VideoPlayer.cs index af99de492..d3feabe8f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/VideoPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/VideoPlayer.cs @@ -3,7 +3,7 @@ using Microsoft.Xna.Framework.Graphics; using System; using System.Xml.Linq; using Barotrauma.Media; -using System.IO; +using Barotrauma.IO; using Microsoft.Xna.Framework.Input; namespace Barotrauma @@ -56,7 +56,7 @@ namespace Barotrauma public VideoPlayer() // GUI elements with size set to Point.Zero are resized based on content { - int screenWidth = (int)(GameMain.GraphicsWidth * 0.55f); + int screenWidth = (int)(GameMain.GraphicsWidth * 0.65f); scaledVideoResolution = new Point(screenWidth, (int)(screenWidth / 16f * 9f)); int width = scaledVideoResolution.X; @@ -178,6 +178,7 @@ namespace Barotrauma videoFrame.RectTransform.NonScaledSize = scaledVideoResolution + new Point(scaledBorderSize, scaledBorderSize); videoView.RectTransform.NonScaledSize = scaledVideoResolution; + videoFrame.RectTransform.AbsoluteOffset = new Point(0, videoFrame.RectTransform.NonScaledSize.Y); title.RectTransform.NonScaledSize = new Point(scaledTextWidth, scaledTitleHeight); title.RectTransform.AbsoluteOffset = new Point((int)(5 * GUI.Scale), (int)(10 * GUI.Scale)); @@ -247,7 +248,7 @@ namespace Barotrauma } else { - videoFrame.RectTransform.AbsoluteOffset = new Point(0, (int)(100 * GUI.Scale)); + videoFrame.RectTransform.AbsoluteOffset = new Point(0, 0); okButton = new GUIButton(new RectTransform(scaledButtonSize, videoFrame.RectTransform, Anchor.TopLeft, Pivot.TopLeft) { AbsoluteOffset = new Point(scaledBorderSize, scaledBorderSize) }, TextManager.Get("Back")) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 03351a772..3d4a8018d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -12,7 +12,7 @@ using System.Diagnostics; using System.Linq; using System.Reflection; using GameAnalyticsSDK.Net; -using System.IO; +using Barotrauma.IO; using System.Threading; using Barotrauma.Tutorials; using Barotrauma.Media; @@ -112,6 +112,8 @@ namespace Barotrauma public event Action OnResolutionChanged; + private bool exiting; + public static GameMain Instance { get; @@ -144,7 +146,17 @@ namespace Barotrauma public static bool WindowActive { - get { return Instance == null || Instance.IsActive; } + get + { + try + { + return Instance != null && !Instance.exiting && Instance.IsActive; + } + catch (NullReferenceException) + { + return false; + } + } } public static GameClient Client; @@ -179,10 +191,14 @@ namespace Barotrauma { Content.RootDirectory = "Content"; - GraphicsDeviceManager = new GraphicsDeviceManager(this); - - GraphicsDeviceManager.IsFullScreen = false; - GraphicsDeviceManager.GraphicsProfile = GfxProfile; +#if DEBUG && WINDOWS + GraphicsAdapter.UseDebugLayers = true; +#endif + GraphicsDeviceManager = new GraphicsDeviceManager(this) + { + IsFullScreen = false, + GraphicsProfile = GfxProfile + }; GraphicsDeviceManager.ApplyChanges(); Window.Title = "Barotrauma"; @@ -329,6 +345,8 @@ namespace Barotrauma //do this here because we need it for the loading screen WaterRenderer.Instance = new WaterRenderer(base.GraphicsDevice, Content); + Quad.Init(GraphicsDevice); + loadingScreenOpen = true; TitleScreen = new LoadingScreen(GraphicsDevice) { @@ -409,7 +427,7 @@ namespace Barotrauma SoundManager.SetCategoryGainMultiplier("ui", Config.SoundVolume, 0); SoundManager.SetCategoryGainMultiplier("waterambience", Config.SoundVolume, 0); SoundManager.SetCategoryGainMultiplier("music", Config.MusicVolume, 0); - SoundManager.SetCategoryGainMultiplier("voip", Config.VoiceChatVolume, 0); + SoundManager.SetCategoryGainMultiplier("voip", Math.Min(Config.VoiceChatVolume, 1.0f), 0); if (Config.EnableSplashScreen && !ConsoleArguments.Contains("-skipintro")) { @@ -514,6 +532,8 @@ namespace Barotrauma ScriptedEventSet.LoadPrefabs(); AfflictionPrefab.LoadAll(GetFilesOfType(ContentType.Afflictions)); SkillSettings.Load(GetFilesOfType(ContentType.SkillSettings)); + Order.Init(); + EventManagerSettings.Init(); TitleScreen.LoadState = 50.0f; yield return CoroutineStatus.Running; @@ -829,6 +849,10 @@ namespace Barotrauma { (GameSession.GameMode as TutorialMode).Tutorial.CloseActiveContentGUI(); } + else if (GameSession.IsTabMenuOpen) + { + gameSession.ToggleTabMenu(); + } else if (GUI.PauseMenuOpen) { GUI.TogglePauseMenu(); @@ -837,7 +861,8 @@ namespace Barotrauma else if ((Character.Controlled == null || !itemHudActive()) //TODO: do we need to check Inventory.SelectedSlot? && Inventory.SelectedSlot == null && CharacterHealth.OpenHealthWindow == null - && !CrewManager.IsCommandInterfaceOpen) + && !CrewManager.IsCommandInterfaceOpen + && !(Screen.Selected is SubEditorScreen editor && !editor.WiringMode && Character.Controlled?.SelectedConstruction != null)) { // Otherwise toggle pausing, unless another window/interface is open. GUI.TogglePauseMenu(); @@ -928,7 +953,7 @@ namespace Barotrauma sw.Stop(); PerformanceCounter.AddElapsedTicks("Update total", sw.ElapsedTicks); - PerformanceCounter.UpdateTimeGraph.Update(sw.ElapsedTicks / (float)TimeSpan.TicksPerMillisecond); + PerformanceCounter.UpdateTimeGraph.Update(sw.ElapsedTicks * 1000.0f / (float)Stopwatch.Frequency); PerformanceCounter.UpdateIterationsGraph.Update(updateIterations); } @@ -950,10 +975,13 @@ namespace Barotrauma double deltaTime = gameTime.ElapsedGameTime.TotalSeconds; - double step = 1.0 / Timing.FrameLimit; - while (!Config.VSyncEnabled && sw.Elapsed.TotalSeconds + deltaTime < step) + if (Timing.FrameLimit > 0) { - Thread.Sleep(1); + double step = 1.0 / Timing.FrameLimit; + while (!Config.VSyncEnabled && sw.Elapsed.TotalSeconds + deltaTime < step) + { + Thread.Sleep(1); + } } PerformanceCounter.Update(sw.Elapsed.TotalSeconds + deltaTime); @@ -978,7 +1006,7 @@ namespace Barotrauma sw.Stop(); PerformanceCounter.AddElapsedTicks("Draw total", sw.ElapsedTicks); - PerformanceCounter.DrawTimeGraph.Update(sw.ElapsedTicks / (float)TimeSpan.TicksPerMillisecond); + PerformanceCounter.DrawTimeGraph.Update(sw.ElapsedTicks * 1000.0f / (float)Stopwatch.Frequency); } @@ -1147,7 +1175,9 @@ namespace Barotrauma protected override void OnExiting(object sender, EventArgs args) { - if (NetworkMember != null) NetworkMember.Disconnect(); + exiting = true; + DebugConsole.NewMessage("Exiting..."); + NetworkMember?.Disconnect(); SteamManager.ShutDown(); try diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index bb2bb7897..160f54f88 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Barotrauma.Steam; namespace Barotrauma { @@ -36,6 +37,9 @@ namespace Barotrauma private bool toggleCrewListOpen = true; private Point crewListEntrySize; + private GUIFrame contextMenu; + private GUIListBox subContextMenu; + /// /// Present only in single player games. In multiplayer. The chatbox is found from GameSession.Client. /// @@ -392,7 +396,19 @@ namespace Barotrauma new RectTransform(crewListEntrySize, parent: crewList.Content.RectTransform, anchor: Anchor.TopRight), style: "CrewListBackground") { - UserData = character + UserData = character, + OnSecondaryClicked = (comp, data) => + { + if (data == null) { return false; } + + var client = GameMain.NetworkMember?.ConnectedClients?.Find(c => c.Character == data); + if (client != null) + { + CreateModerationContextMenu(PlayerInput.MousePosition.ToPoint(), client); + return true; + } + return false; + } }; var iconRelativeWidth = (float)crewListEntrySize.Y / background.Rect.Width; @@ -469,7 +485,7 @@ namespace Barotrauma characterButton.ToolTip = characterTooltip; if (character.Info?.Job?.Prefab != null) { - characterButton.TooltipColorData = new List() { new ColorData() + characterButton.TooltipRichTextData = new List() { new RichTextData() { Color = character.Info.Job.Prefab.UIColor, EndIndex = characterTooltip.Length - 1 @@ -679,6 +695,8 @@ namespace Barotrauma #endregion + #region Crew List Order Displayment + /// /// Sets the character's current order (if it's close enough to receive messages from orderGiver) and /// displays the order in the crew UI @@ -689,7 +707,7 @@ namespace Barotrauma { if (orderGiver == null || orderGiver.CurrentHull == null) { return; } var hull = orderGiver.CurrentHull; - AddOrder(new Order(order.Prefab, hull, null, orderGiver), order.Prefab.FadeOutTime); + AddOrder(new Order(order.Prefab ?? order, hull, null, orderGiver), order.FadeOutTime); if (IsSinglePlayer) { orderGiver.Speak( @@ -703,11 +721,8 @@ namespace Barotrauma } else { - if (character == null) - { - //can't issue an order if no characters are available - return; - } + //can't issue an order if no characters are available + if (character == null) { return; } if (IsSinglePlayer) { @@ -717,7 +732,7 @@ namespace Barotrauma } else if (orderGiver != null) { - OrderChatMessage msg = new OrderChatMessage(order, option, order?.TargetItemComponent?.Item, character, orderGiver); + OrderChatMessage msg = new OrderChatMessage(order, option, order?.TargetEntity ?? order?.TargetItemComponent?.Item, character, orderGiver); GameMain.Client?.SendChatMessage(msg); } } @@ -867,6 +882,8 @@ namespace Barotrauma } } + #endregion + #region Updating and drawing the UI private void DrawMiniMapOverlay(SpriteBatch spriteBatch, GUICustomComponent container) @@ -895,6 +912,213 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, center + start, center + end, Color.DarkCyan * Rand.Range(0.3f, 0.35f), width: 10); } } + + #region Context Menu + + public void CreateModerationContextMenu(Point mousePos, Client client) + { + if (IsSinglePlayer || client == null || (GameMain.NetworkMember?.ConnectedClients?.All(match => match != client) ?? true)) { return; } + + contextMenu = new GUIFrame(new RectTransform(new Vector2(0.1f, 0.12f), GUI.Canvas) { ScreenSpaceOffset = mousePos }, style: "GUIToolTip") { UserData = client }; + + var nameLabel = new GUITextBlock(new RectTransform(new Vector2(1f, 0.2f), contextMenu.RectTransform), client.Name, font: GUI.SubHeadingFont) + { + Padding = new Vector4(8), + TextColor = client.Character?.Info?.Job.Prefab.UIColor ?? Color.White + }; + + var optionsList = new GUIListBox(new RectTransform(new Vector2(1f, 0.8f), contextMenu.RectTransform, Anchor.BottomLeft), style: null) + { + Padding = new Vector4(4, 0, 4, 4) + }; + + bool hasSteam = client.SteamID > 0 && SteamManager.IsInitialized, + canKick = GameMain.Client.HasPermission(ClientPermissions.Kick), + canBan = GameMain.Client.HasPermission(ClientPermissions.Ban) && client.AllowKicking, + canPromo = GameMain.Client.HasPermission(ClientPermissions.ManagePermissions); + + // Disable options if we are targeting ourselves + if (client.ID == GameMain.Client?.ID) + { + canKick = canBan = canPromo = false; + } + + RectTransform parent = optionsList.Content.RectTransform; + new GUITextBlock(new RectTransform(Point.Zero, parent), TextManager.Get("viewsteamprofile"), font: GUI.SmallFont) + { + Padding = new Vector4(4), + Enabled = hasSteam, + UserData = "steam" + }; + + new GUITextBlock(new RectTransform(Point.Zero, parent), TextManager.Get("permissions"), font: GUI.SmallFont) + { + Padding = new Vector4(4), + Enabled = canPromo, + UserData = "promote" + }; + + new GUITextBlock(new RectTransform(Point.Zero, parent), TextManager.Get(client.MutedLocally ? "unmute" : "mute"), font: GUI.SmallFont) + { + Padding = new Vector4(4), + Enabled = client.ID != GameMain.Client?.ID, + UserData = "mute" + }; + + new GUITextBlock(new RectTransform(Point.Zero, parent), TextManager.Get(canKick ? "kick" : "votetokick"), font: GUI.SmallFont) + { + Padding = new Vector4(4), + Enabled = client.ID != GameMain.Client?.ID && client.AllowKicking, + UserData = canKick ? "kick" : "votekick" + }; + + new GUITextBlock(new RectTransform(Point.Zero, parent), TextManager.Get("ban"), font: GUI.SmallFont) + { + Padding = new Vector4(4), + Enabled = canBan, + UserData = "ban" + }; + + foreach (GUIComponent c in optionsList.Content.Children) + { + if (c is GUITextBlock child && !child.Enabled) + { + child.TextColor *= 0.5f; + } + } + + var children = optionsList.Content.Children.ToList(); + + // Resize all children to the size of their text + foreach (GUITextBlock block in children.Where(c => c is GUITextBlock).Cast()) + { + block.RectTransform.NonScaledSize = new Point((int) (block.TextSize.X + (block.Padding.X + block.Padding.Z)), (int)(18 * GUI.Scale)); + } + + int horizontalPadding = (int)(optionsList.Padding.X + optionsList.Padding.Z); + int verticalPadding = (int)(optionsList.Padding.Y + optionsList.Padding.W); + int largestWidth = children.Max(c => c.Rect.Width + horizontalPadding); + + // If the name is bigger than any of the options then overwrite + nameLabel.RectTransform.MinSize = new Point((int)(nameLabel.TextSize.X + (nameLabel.Padding.X + nameLabel.Padding.Z)), nameLabel.RectTransform.NonScaledSize.Y); + if (largestWidth < nameLabel.RectTransform.MinSize.X) { largestWidth = nameLabel.RectTransform.MinSize.X; } + + // Resize all children to the size of the longest element + foreach (GUIComponent c in children) { c.RectTransform.MinSize = new Point(largestWidth, c.Rect.Height); } + + // crop the context menu + contextMenu.RectTransform.NonScaledSize = new Point(largestWidth, (children.Sum(c => c.Rect.Height) + verticalPadding) + nameLabel.Rect.Height); + + // if the menu would go off the screen then move it up + if (contextMenu.Rect.Bottom > GameMain.GraphicsHeight) + { + contextMenu.RectTransform.ScreenSpaceOffset = new Point(mousePos.X, mousePos.Y - contextMenu.Rect.Height); + } + + optionsList.OnSelected = (component, obj) => + { + if (component.Enabled) + { + switch (obj) + { + case "steam": + Steamworks.SteamFriends.OpenWebOverlay($"https://steamcommunity.com/profiles/{client.SteamID}"); + break; + case "mute": + client.MutedLocally = !client.MutedLocally; + break; + case "kick": + GameMain.Client?.CreateKickReasonPrompt(client.Name, false); + break; + case "votekick": + GameMain.Client?.VoteForKick(client); + break; + case "ban": + GameMain.Client?.CreateKickReasonPrompt(client.Name, true); + break; + } + contextMenu = null; + return true; + } + return false; + }; + } + + private void CreatePromoteSubMenu(Point pos, Client client) + { + if (client == null ) { return; } + + subContextMenu = new GUIListBox(new RectTransform(new Vector2(0.1f, 0.1f), GUI.Canvas) { ScreenSpaceOffset = pos }, style: "GUIToolTip"); + + foreach (var rank in PermissionPreset.List) + { + new GUITextBlock(new RectTransform(Point.Zero, subContextMenu.Content.RectTransform), rank.Name, font: GUI.SmallFont) + { + ToolTip = rank.Description, + UserData = rank, + Padding = new Vector4(4) + }; + } + + var children = subContextMenu.Content.Children.ToList(); + + // Resize all children to the size of their text + foreach (GUITextBlock block in children.Where(c => c is GUITextBlock).Cast()) + { + block.RectTransform.NonScaledSize = new Point((int) (block.TextSize.X + (block.Padding.X + block.Padding.Z)), (int)(18 * GUI.Scale)); + } + + int horizontalPadding = (int)(subContextMenu.Padding.X + subContextMenu.Padding.Z); + int largestWidth = children.Max(c => c.Rect.Width + horizontalPadding); + + // Resize all children to the size of the longest element + foreach (GUIComponent c in children) { c.RectTransform.MinSize = new Point(largestWidth, c.Rect.Height); } + + // crop the context menu + subContextMenu.RectTransform.NonScaledSize = new Point(largestWidth, children.Sum(c => c.Rect.Height) + horizontalPadding); + + // if the menu would go off the screen then move it up + if (subContextMenu.Rect.Bottom > GameMain.GraphicsHeight) + { + subContextMenu.RectTransform.ScreenSpaceOffset = new Point(pos.X, pos.Y - subContextMenu.Rect.Height); + } + + subContextMenu.OnSelected = (component, obj) => + { + if (component.Enabled && obj is PermissionPreset preset) + { + var label = TextManager.GetWithVariables(preset.Permissions == ClientPermissions.None ? "clearrankprompt" : "giverankprompt", new []{ "[user]", "[rank]" }, new []{ client.Name, preset.Name }); + + var msgBox = new GUIMessageBox(string.Empty, label, new[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); + + msgBox.Buttons[0].OnClicked = (yesBtn, userdata) => + { + client.SetPermissions(preset.Permissions, preset.PermittedCommands); + GameMain.Client.UpdateClientPermissions(client); + msgBox.Close(); + return true; + }; + msgBox.Buttons[1].OnClicked = (_, userdata) => + { + msgBox.Close(); + return true; + }; + contextMenu = null; + subContextMenu = null; + return true; + } + return false; + }; + } + + private static bool IsMouseOnContextMenu(Rectangle rect) + { + Rectangle expandedRect = rect; + expandedRect.Inflate(20, 20); + return expandedRect.Contains(PlayerInput.MousePosition); + } + + #endregion public void AddToGUIUpdateList() { @@ -922,6 +1146,8 @@ namespace Barotrauma } guiFrame.AddToGUIUpdateList(); + contextMenu?.AddToGUIUpdateList(false, 1); + subContextMenu?.AddToGUIUpdateList(false, 1); } public void SelectNextCharacter() @@ -985,6 +1211,43 @@ namespace Barotrauma SelectPreviousCharacter(); } } + + // context menu behavior + if (contextMenu != null) + { + var promote = contextMenu.GetChild()?.Content.GetChildByUserData("promote"); + + if (promote != null && promote.Enabled) + { + promote.ExternalHighlight = subContextMenu != null; + + if (GUI.IsMouseOn(promote)) + { + if (contextMenu.UserData is Client client && subContextMenu == null) + { + CreatePromoteSubMenu(new Point(promote.Rect.Right, promote.Rect.Y), client); + } + } + else if (subContextMenu != null && !IsMouseOnContextMenu(subContextMenu.Rect)) + { + subContextMenu = null; + } + } + else + { + subContextMenu = null; + } + + if (subContextMenu == null && !IsMouseOnContextMenu(contextMenu.Rect)) + { + contextMenu = null; + } + } + + if (contextMenu == null && subContextMenu != null) + { + subContextMenu = null; + } if (GUI.DisableHUD) { return; } @@ -995,7 +1258,14 @@ namespace Barotrauma if (PlayerInput.KeyDown(InputType.Command) && (GUI.KeyboardDispatcher.Subscriber == null || GUI.KeyboardDispatcher.Subscriber == crewList) && commandFrame == null && !clicklessSelectionActive && CanIssueOrders) { - CreateCommandUI(HUDLayoutSettings.PortraitArea.Contains(PlayerInput.MousePosition) ? Character.Controlled : GUI.MouseOn?.UserData as Character); + if (PlayerInput.KeyDown(Keys.LeftShift) || PlayerInput.KeyDown(Keys.RightShift)) + { + CreateCommandUI(FindEntityContext(), true); + } + else + { + CreateCommandUI(HUDLayoutSettings.BottomRightInfoArea.Contains(PlayerInput.MousePosition) ? Character.Controlled : GUI.MouseOn?.UserData as Character); + } GUI.PlayUISound(GUISoundType.PopupMenu); clicklessSelectionActive = isOpeningClick = true; } @@ -1018,19 +1288,9 @@ namespace Barotrauma else if (PlayerInput.SecondaryMouseButtonClicked() && characterContext == null && (optionNodes.Any(n => GUI.IsMouseOn(n.Item1)) || shortcutNodes.Any(n => GUI.IsMouseOn(n)))) { - var node = optionNodes.Find(n => GUI.IsMouseOn(n.Item1))?.Item1; - if (node == null) - { - node = shortcutNodes.Find(n => GUI.IsMouseOn(n)); - } - // Make sure the node is for an option-less order... - if (node.UserData is Order order && - !(order.ItemComponentType != null || order.ItemIdentifiers.Length > 0 || order.Options.Length > 1)) - { - CreateAssignmentNodes(node); - } - // ...or an order option - else if (node.UserData is Tuple) + var node = optionNodes.Find(n => GUI.IsMouseOn(n.Item1))?.Item1 ?? shortcutNodes.Find(n => GUI.IsMouseOn(n)); + // Make sure the node is for an option-less order or an order option + if ((node.UserData is Order order && !order.HasOptions && (!order.MustSetTarget || itemContext != null)) || node.UserData is Tuple) { CreateAssignmentNodes(node); } @@ -1091,7 +1351,7 @@ namespace Barotrauma } } - if (closestNode == selectedNode) + if (closestNode != null && closestNode == selectedNode) { timeSelected += deltaTime; if (timeSelected >= selectionTime) @@ -1116,6 +1376,32 @@ namespace Barotrauma } } + if (assignmentNodeIcons.Any()) + { + if (PlayerInput.KeyDown(Keys.LeftShift) || PlayerInput.KeyDown(Keys.RightShift)) + { + if (assignmentNodeIcons.First().OrderIcon.Visible) + { + foreach (AssignmentNodeIconSet set in assignmentNodeIcons) + { + set.OrderIcon.Visible = false; + set.JobIcon.Visible = true; + } + } + } + else + { + if (assignmentNodeIcons.First().JobIcon.Visible) + { + foreach (AssignmentNodeIconSet set in assignmentNodeIcons) + { + set.JobIcon.Visible = false; + set.OrderIcon.Visible = true; + } + } + } + } + var hotkeyHit = false; foreach (Tuple node in optionNodes) { @@ -1162,8 +1448,6 @@ namespace Barotrauma #endregion - if (GUI.DisableUpperHUD) { return; } - if (ChatBox != null) { ChatBox.Update(deltaTime); @@ -1205,49 +1489,52 @@ namespace Barotrauma } } - crewArea.Visible = characters.Count > 0 && CharacterHealth.OpenHealthWindow == null; - - foreach (GUIComponent child in crewList.Content.Children) + if (!GUI.DisableUpperHUD) { - if (child.UserData is Character character) + crewArea.Visible = characters.Count > 0 && CharacterHealth.OpenHealthWindow == null; + + foreach (GUIComponent child in crewList.Content.Children) { - child.Visible = Character.Controlled == null || Character.Controlled.TeamID == character.TeamID; - if (child.Visible) + if (child.UserData is Character character) { - if (character == Character.Controlled && child.State != GUIComponent.ComponentState.Selected) + child.Visible = Character.Controlled == null || Character.Controlled.TeamID == character.TeamID; + if (child.Visible) { - crewList.Select(character, force: true); - } - if (child.FindChild(c => c is GUILayoutGroup) is GUILayoutGroup layoutGroup) - { - if (GetCurrentOrderComponent(layoutGroup) is GUIComponent orderButton && - orderButton.GetChildByUserData("colorsource") is GUIComponent orderIcon && - orderButton.GetChildByUserData("cancel") is GUIComponent cancelIcon) + if (character == Character.Controlled && child.State != GUIComponent.ComponentState.Selected) { - cancelIcon.Visible = GUI.IsMouseOn(orderIcon); + crewList.Select(character, force: true); } - if (layoutGroup.GetChildByUserData("soundicons")? - .FindChild(c => c.UserData is Pair pair && pair.First == "soundicon") is GUIImage soundIcon) + if (child.FindChild(c => c is GUILayoutGroup) is GUILayoutGroup layoutGroup) { - VoipClient.UpdateVoiceIndicator(soundIcon, 0.0f, deltaTime); + if (GetCurrentOrderComponent(layoutGroup) is GUIComponent orderButton && + orderButton.GetChildByUserData("colorsource") is GUIComponent orderIcon && + orderButton.GetChildByUserData("cancel") is GUIComponent cancelIcon) + { + cancelIcon.Visible = GUI.IsMouseOn(orderIcon); + } + if (layoutGroup.GetChildByUserData("soundicons")? + .FindChild(c => c.UserData is Pair pair && pair.First == "soundicon") is GUIImage soundIcon) + { + VoipClient.UpdateVoiceIndicator(soundIcon, 0.0f, deltaTime); + } } } } } - } - crewArea.RectTransform.AbsoluteOffset = Vector2.SmoothStep( - new Vector2(-crewArea.Rect.Width - HUDLayoutSettings.Padding, 0.0f), - Vector2.Zero, - crewListOpenState).ToPoint(); - crewListOpenState = ToggleCrewListOpen ? - Math.Min(crewListOpenState + deltaTime * 2.0f, 1.0f) : - Math.Max(crewListOpenState - deltaTime * 2.0f, 0.0f); + crewArea.RectTransform.AbsoluteOffset = Vector2.SmoothStep( + new Vector2(-crewArea.Rect.Width - HUDLayoutSettings.Padding, 0.0f), + Vector2.Zero, + crewListOpenState).ToPoint(); + crewListOpenState = ToggleCrewListOpen ? + Math.Min(crewListOpenState + deltaTime * 2.0f, 1.0f) : + Math.Max(crewListOpenState - deltaTime * 2.0f, 0.0f); - if (GUI.KeyboardDispatcher.Subscriber == null && PlayerInput.KeyHit(InputType.CrewOrders)) - { - GUI.PlayUISound(GUISoundType.PopupMenu); - ToggleCrewListOpen = !ToggleCrewListOpen; + if (GUI.KeyboardDispatcher.Subscriber == null && PlayerInput.KeyHit(InputType.CrewOrders)) + { + GUI.PlayUISound(GUISoundType.PopupMenu); + ToggleCrewListOpen = !ToggleCrewListOpen; + } } UpdateReports(); @@ -1273,34 +1560,51 @@ namespace Barotrauma } private GUIFrame commandFrame, targetFrame; private GUIButton centerNode, returnNode, expandNode, shortcutCenterNode; - private List> optionNodes = new List>(); + private readonly List> optionNodes = new List>(); private Keys returnNodeHotkey = Keys.None, expandNodeHotkey = Keys.None; - private List shortcutNodes = new List(); - private List extraOptionNodes = new List(); + private readonly List shortcutNodes = new List(); + private readonly List extraOptionNodes = new List(); private GUICustomComponent nodeConnectors; private GUIImage background; private GUIButton selectedNode; - private float selectionTime = 0.75f, timeSelected = 0.0f; + private readonly float selectionTime = 0.75f; + private float timeSelected = 0.0f; private bool clicklessSelectionActive, isOpeningClick, isSelectionHighlighted; - private Point centerNodeSize, nodeSize, shortcutCenterNodeSize, shortcutNodeSize, returnNodeSize; + private Point centerNodeSize, nodeSize, shortcutCenterNodeSize, shortcutNodeSize, returnNodeSize, assignmentNodeSize; private float centerNodeMargin, optionNodeMargin, shortcutCenterNodeMargin, shortcutNodeMargin, returnNodeMargin; private List availableCategories; private Stack historyNodes = new Stack(); - private List extraOptionCharacters = new List(); + private readonly List extraOptionCharacters = new List(); + private readonly List assignmentNodeIcons = new List(); + private struct AssignmentNodeIconSet + { + public GUIImage OrderIcon { get; private set; } + public GUIImage JobIcon { get; private set; } + public AssignmentNodeIconSet(GUIImage orderIcon, GUIImage jobIcon) + { + OrderIcon = orderIcon; + JobIcon = jobIcon; + } + } /// /// node.Color = node.HighlightColor * nodeColorMultiplier /// private const float nodeColorMultiplier = 0.75f; - private const int assignmentNodeMaxCount = 8; private int nodeDistance = (int)(GUI.Scale * 250); - private float returnNodeDistanceModifier = 0.65f; + private const float returnNodeDistanceModifier = 0.65f; private Order dismissedOrderPrefab; private Character characterContext; + private Item itemContext; + private Hull hullContext; + private bool isContextual; + private readonly List contextualOrders = new List(); private Point shorcutCenterNodeOffset; + private const int maxShorcutNodeCount = 4; + private bool WasCommandInterfaceDisabledThisUpdate { get; set; } private bool CanIssueOrders { @@ -1309,7 +1613,7 @@ namespace Barotrauma #if DEBUG return Character.Controlled == null || Character.Controlled.Info != null && Character.Controlled.SpeechImpediment < 100.0f; #else - return Character.Controlled != null && Character.Controlled.SpeechImpediment < 100.0f; + return Character.Controlled?.Info != null && Character.Controlled.SpeechImpediment < 100.0f; #endif } } @@ -1323,11 +1627,65 @@ namespace Barotrauma #endif } - private void CreateCommandUI(Character characterContext = null) + private Entity FindEntityContext() + { + if (Character.Controlled?.FocusedCharacter != null) + { + if (Character.Controlled?.FocusedItem != null) + { + Vector2 mousePos = GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition); + if (Vector2.Distance(mousePos, Character.Controlled.FocusedCharacter.WorldPosition) < Vector2.Distance(mousePos, Character.Controlled.FocusedItem.WorldPosition)) + { + return Character.Controlled.FocusedCharacter; + } + else + { + return Character.Controlled.FocusedItem; + } + } + else + { + return Character.Controlled.FocusedCharacter; + } + + } + else if (TryGetBreachedHullAtHoveredWall(out Hull breachedHull)) + { + return breachedHull; + } + else + { + return Character.Controlled?.FocusedItem; + } + } + + private void CreateCommandUI(Entity entityContext = null, bool forceContextual = false) { if (commandFrame != null) { DisableCommandUI(); } + CharacterHealth.OpenHealthWindow = null; + + // Character context works differently to others as we still use the "basic" command interface, + // but the order will be automatically assigned to this character + isContextual = forceContextual; + if (entityContext is Character character) + { + characterContext = character; + isContextual = false; + } + else if (entityContext is Item item) + { + itemContext = item; + isContextual = true; + } + else if (entityContext is Hull hull) + { + hullContext = hull; + isContextual = true; + } + ScaleCommandUI(); + commandFrame = new GUIFrame( new RectTransform(Vector2.One, GUICanvas.Instance, anchor: Anchor.Center), style: null, @@ -1336,8 +1694,6 @@ namespace Barotrauma new RectTransform(Vector2.One, commandFrame.RectTransform, anchor: Anchor.Center), "CommandBackground"); background.Color = background.Color * 0.8f; - - this.characterContext = characterContext; GUIButton startNode = null; if (characterContext == null) { @@ -1367,7 +1723,11 @@ namespace Barotrauma new RectTransform(Vector2.One, startNode.RectTransform, anchor: Anchor.Center), (spriteBatch, _) => { - characterContext.Info.DrawIcon(spriteBatch, startNode.Center, startNode.Rect.Size.ToVector2() * 0.6f); + if (!(entityContext is Character character)) { return; } + var node = startNode; + character.Info.DrawJobIcon(spriteBatch, + new Rectangle((int)(node.Rect.X + node.Rect.Width * 0.5f), (int)(node.Rect.Y + node.Rect.Height * 0.1f), (int)(node.Rect.Width * 0.6f), (int)(node.Rect.Height * 0.8f))); + character.Info.DrawIcon(spriteBatch, new Vector2(node.Rect.X + node.Rect.Width * 0.35f, node.Center.Y), node.Rect.Size.ToVector2() * 0.7f); }) { ToolTip = characterContext.Info.DisplayName + " (" + characterContext.Info.Job.Name + ")" @@ -1378,8 +1738,16 @@ namespace Barotrauma availableCategories ??= GetAvailableCategories(); dismissedOrderPrefab ??= Order.GetPrefab("dismissed"); - CreateShortcutNodes(); - CreateOrderCategoryNodes(); + if (isContextual) + { + CreateContextualOrderNodes(); + } + else + { + CreateShortcutNodes(); + CreateOrderCategoryNodes(); + } + CreateNodeConnectors(); if (Character.Controlled != null) { @@ -1404,16 +1772,21 @@ namespace Barotrauma private void ScaleCommandUI() { - centerNodeSize = new Point((int)(100 * GUI.Scale)); + // Node sizes nodeSize = new Point((int)(100 * GUI.Scale)); - shortcutCenterNodeSize = new Point((int)(48 * GUI.Scale)); - shortcutNodeSize = new Point((int)(64 * GUI.Scale)); + centerNodeSize = nodeSize; returnNodeSize = new Point((int)(48 * GUI.Scale)); + assignmentNodeSize = new Point((int)(64 * GUI.Scale)); + shortcutCenterNodeSize = returnNodeSize; + shortcutNodeSize = assignmentNodeSize; + + // Node margins (used in drawing the connecting lines) centerNodeMargin = centerNodeSize.X * 0.5f; optionNodeMargin = nodeSize.X * 0.5f; shortcutCenterNodeMargin = shortcutCenterNodeSize.X * 0.45f; shortcutNodeMargin = shortcutNodeSize.X * 0.5f; returnNodeMargin = returnNodeSize.X * 0.5f; + nodeDistance = (int)(150 * GUI.Scale); shorcutCenterNodeOffset = new Point(0, (int)(1.25f * nodeDistance)); } @@ -1423,7 +1796,6 @@ namespace Barotrauma availableCategories = new List(); foreach (OrderCategory category in Enum.GetValues(typeof(OrderCategory))) { - if (category == OrderCategory.Undefined) { continue; } if (Order.PrefabList.Any(o => o.Category == category && !o.TargetAllCharacters)) { availableCategories.Add(category); @@ -1445,12 +1817,12 @@ namespace Barotrauma { if (centerNode == null || optionNodes == null) { return; } var startNodePos = centerNode.Rect.Center.ToVector2(); - if (targetFrame == null || !targetFrame.Visible) + // Don't draw connectors for mini map options or assignment nodes + if ((targetFrame == null || !targetFrame.Visible) && !(optionNodes.FirstOrDefault()?.Item1.UserData is Character)) { optionNodes.ForEach(n => DrawNodeConnector(startNodePos, centerNodeMargin, n.Item1, optionNodeMargin, spriteBatch)); } DrawNodeConnector(startNodePos, centerNodeMargin, returnNode, returnNodeMargin, spriteBatch); - DrawNodeConnector(startNodePos, centerNodeMargin, expandNode, optionNodeMargin, spriteBatch); if (shortcutCenterNode == null || !shortcutCenterNode.Visible) { return; } DrawNodeConnector(startNodePos, centerNodeMargin, shortcutCenterNode, shortcutCenterNodeMargin, spriteBatch); startNodePos = shortcutCenterNode.Rect.Center.ToVector2(); @@ -1493,8 +1865,12 @@ namespace Barotrauma background = null; commandFrame = null; extraOptionCharacters.Clear(); + assignmentNodeIcons.Clear(); isOpeningClick = isSelectionHighlighted = false; characterContext = null; + itemContext = null; + isContextual = false; + contextualOrders.Clear(); returnNodeHotkey = expandNodeHotkey = Keys.None; if (Character.Controlled != null) { @@ -1509,6 +1885,7 @@ namespace Barotrauma shortcutNodes.Remove(node); }; RemoveOptionNodes(); + if (returnNode != null) { returnNode.RemoveChild(returnNode.GetChildByUserData("hotkey")); @@ -1516,15 +1893,20 @@ namespace Barotrauma returnNode.Visible = false; historyNodes.Push(returnNode); } - SetReturnNode(centerNode, new Point( - (int)(node.RectTransform.AbsoluteOffset.X * -returnNodeDistanceModifier), - (int)(node.RectTransform.AbsoluteOffset.Y * -returnNodeDistanceModifier))); + + // When the mini map is shown, always position the return node on the bottom + var offset = node?.UserData is Order order && order.GetMatchingItems(true).Count > 1 ? + new Point(0, (int)(returnNodeDistanceModifier * nodeDistance)) : + node.RectTransform.AbsoluteOffset.Multiply(-returnNodeDistanceModifier); + SetReturnNode(centerNode, offset); + SetCenterNode(node); if (shortcutCenterNode != null) { commandFrame.RemoveChild(shortcutCenterNode); shortcutCenterNode = null; } + CreateNodes(userData); CreateReturnNodeHotkey(); return true; @@ -1573,9 +1955,14 @@ namespace Barotrauma } } - private void SetCenterNode(GUIButton node) + private void SetCenterNode(GUIButton node, bool resetAnchor = false) { node.RectTransform.Parent = commandFrame.RectTransform; + if (resetAnchor) + { + node.RectTransform.SetPosition(Anchor.Center); + } + node.RectTransform.SetPosition(Anchor.Center); node.RectTransform.MoveOverTime(Point.Zero, CommandNodeAnimDuration); node.RectTransform.ScaleOverTime(centerNodeSize, CommandNodeAnimDuration); node.RemoveChild(node.GetChildByUserData("hotkey")); @@ -1610,8 +1997,15 @@ namespace Barotrauma { if (userData == null) { - CreateShortcutNodes(); - CreateOrderCategoryNodes(); + if (isContextual) + { + CreateContextualOrderNodes(); + } + else + { + CreateShortcutNodes(); + CreateOrderCategoryNodes(); + } } else if (userData is OrderCategory category) { @@ -1634,6 +2028,7 @@ namespace Barotrauma expandNode = null; expandNodeHotkey = Keys.None; RemoveExtraOptionNodes(); + assignmentNodeIcons.Clear(); } private void RemoveExtraOptionNodes() @@ -1673,15 +2068,13 @@ namespace Barotrauma private void CreateShortcutNodes() { - var sub = Character.Controlled != null && Character.Controlled.TeamID == Character.TeamType.Team2 && Submarine.MainSubs.Length > 1 ? - Submarine.MainSubs[1] : Submarine.MainSub; + Submarine sub = GetTargetSubmarine(); if (sub == null) { return; } shortcutNodes.Clear(); - var reactor = sub.GetItems(false).Find(i => i.HasTag("reactor"))?.GetComponent(); - if (reactor != null) + if (shortcutNodes.Count < maxShorcutNodeCount && sub.GetItems(false).Find(i => i.HasTag("reactor") && !i.NonInteractable)?.GetComponent() is Reactor reactor) { var reactorOutput = -reactor.CurrPowerConsumption; // If player is not an engineer AND the reactor is not powered up AND nobody is using the reactor @@ -1690,16 +2083,17 @@ namespace Barotrauma reactorOutput < float.Epsilon && characters.None(c => c.SelectedConstruction == reactor.Item)) { var order = new Order(Order.GetPrefab("operatereactor"), reactor.Item, reactor, Character.Controlled); + var option = order.Prefab.Options[0]; shortcutNodes.Add( - CreateOrderOptionNode(shortcutNodeSize, null, Point.Zero, order, order.Prefab.Options[0], order.Prefab.OptionNames[0], -1)); + CreateOrderOptionNode(shortcutNodeSize, null, Point.Zero, order, option, order.Prefab.GetOptionName(option), -1)); } } // TODO: Reconsider the conditions as bot captain can have the nav term selected without operating it // If player is not a captain AND nobody is using the nav terminal AND the nav terminal is powered up // --> Create shortcut node for Steer order - if ((Character.Controlled == null || Character.Controlled.Info.Job.Prefab != JobPrefab.Get("captain")) && - sub.GetItems(false).Find(i => i.HasTag("navterminal")) is Item nav && characters.None(c => c.SelectedConstruction == nav) && + if (shortcutNodes.Count < maxShorcutNodeCount && (Character.Controlled == null || Character.Controlled.Info.Job.Prefab != JobPrefab.Get("captain")) && + sub.GetItems(false).Find(i => i.HasTag("navterminal") && !i.NonInteractable) is Item nav && characters.None(c => c.SelectedConstruction == nav) && nav.GetComponent() is Steering steering && steering.Voltage > steering.MinVoltage) { shortcutNodes.Add( @@ -1708,7 +2102,7 @@ namespace Barotrauma // If player is not a security officer AND invaders are reported // --> Create shorcut node for Fight Intruders order - if ((Character.Controlled == null || Character.Controlled.Info.Job.Prefab != JobPrefab.Get("securityofficer")) && + if (shortcutNodes.Count < maxShorcutNodeCount && (Character.Controlled == null || Character.Controlled.Info.Job.Prefab != JobPrefab.Get("securityofficer")) && (Order.GetPrefab("reportintruders") is Order reportIntruders && ActiveOrders.Any(o => o.First.Prefab == reportIntruders))) { shortcutNodes.Add( @@ -1717,7 +2111,7 @@ namespace Barotrauma // If player is not a mechanic AND a breach has been reported // --> Create shorcut node for Fix Leaks order - if ((Character.Controlled == null || Character.Controlled.Info.Job.Prefab != JobPrefab.Get("mechanic")) && + if (shortcutNodes.Count < maxShorcutNodeCount && (Character.Controlled == null || Character.Controlled.Info.Job.Prefab != JobPrefab.Get("mechanic")) && (Order.GetPrefab("reportbreach") is Order reportBreach && ActiveOrders.Any(o => o.First.Prefab == reportBreach))) { shortcutNodes.Add( @@ -1726,7 +2120,7 @@ namespace Barotrauma // If player is not an engineer AND broken devices have been reported // --> Create shortcut node for Repair Damaged Systems order - if ((Character.Controlled == null || Character.Controlled.Info.Job.Prefab != JobPrefab.Get("engineer")) && + if (shortcutNodes.Count < maxShorcutNodeCount && (Character.Controlled == null || Character.Controlled.Info.Job.Prefab != JobPrefab.Get("engineer")) && (Order.GetPrefab("reportbrokendevices") is Order reportBrokenDevices && ActiveOrders.Any(o => o.First.Prefab == reportBrokenDevices))) { shortcutNodes.Add( @@ -1735,12 +2129,30 @@ namespace Barotrauma // If fire is reported // --> Create shortcut node for Extinguish Fires order - if (ActiveOrders.Any(o=> o.First.Prefab == Order.GetPrefab("reportfire"))) + if (shortcutNodes.Count < maxShorcutNodeCount && ActiveOrders.Any(o=> o.First.Prefab == Order.GetPrefab("reportfire"))) { shortcutNodes.Add( CreateOrderNode(shortcutNodeSize, null, Point.Zero, Order.GetPrefab("extinguishfires"), -1)); } + if (shortcutNodes.Count < maxShorcutNodeCount && characterContext?.Info?.Job?.Prefab?.AppropriateOrders != null) + { + foreach (string orderIdentifier in characterContext.Info.Job.Prefab.AppropriateOrders) + { + if (Order.GetPrefab(orderIdentifier) is Order orderPrefab && + shortcutNodes.None(n => (n.UserData is Order order && order.Identifier == orderIdentifier) || + (n.UserData is Tuple orderWithOption && orderWithOption.Item1.Identifier == orderIdentifier)) && + !orderPrefab.TargetAllCharacters && orderPrefab.Category != null) + { + if (!orderPrefab.MustSetTarget || orderPrefab.GetMatchingItems(sub, true).Any()) + { + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, orderPrefab, -1)); + } + if (shortcutNodes.Count >= maxShorcutNodeCount) { break; } + } + } + } + if (shortcutNodes.Count < 1) { return; } shortcutCenterNode = new GUIButton( @@ -1757,27 +2169,139 @@ namespace Barotrauma var nodeCountForCalculations = shortcutNodes.Count * 2 + 2; var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, 0.75f * nodeDistance, nodeCountForCalculations); + var firstOffsetIndex = nodeCountForCalculations / 2 - 1; for (int i = 0; i < shortcutNodes.Count; i++) { shortcutNodes[i].RectTransform.Parent = commandFrame.RectTransform; - shortcutNodes[i].RectTransform.MoveOverTime(shorcutCenterNodeOffset + offsets[i + 1].ToPoint(), CommandNodeAnimDuration); + shortcutNodes[i].RectTransform.MoveOverTime(shorcutCenterNodeOffset + offsets[firstOffsetIndex - i].ToPoint(), CommandNodeAnimDuration); } } private void CreateOrderNodes(OrderCategory orderCategory) { var orders = Order.PrefabList.FindAll(o => o.Category == orderCategory && !o.TargetAllCharacters); + Order order; + bool disableNode; var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, GetCircumferencePointCount(orders.Count), GetFirstNodeAngle(orders.Count)); for (int i = 0; i < orders.Count; i++) { + order = orders[i]; + disableNode = !CanSomeoneHearCharacter() || + (order.MustSetTarget && (order.ItemComponentType != null || order.ItemIdentifiers.Length > 0) && order.GetMatchingItems(true).None()); optionNodes.Add(new Tuple( - CreateOrderNode(nodeSize, commandFrame.RectTransform, offsets[i].ToPoint(), orders[i], (i + 1) % 10), - CanSomeoneHearCharacter() ? Keys.D0 + (i + 1) % 10 : Keys.None)); + CreateOrderNode(nodeSize, commandFrame.RectTransform, offsets[i].ToPoint(), order, (i + 1) % 10, disableNode: disableNode, checkIfOrderCanBeHeard: false), + !disableNode ? Keys.D0 + (i + 1) % 10 : Keys.None)); } } - private GUIButton CreateOrderNode(Point size, RectTransform parent, Point offset, Order order, int hotkey) + /// + /// Create order nodes based on the item context + /// + private void CreateContextualOrderNodes() + { + if (contextualOrders.None()) + { + // Check if targeting an item or a hull + if (itemContext != null && !itemContext.NonInteractable) + { + foreach (Order p in Order.PrefabList) + { + if ((p.ItemIdentifiers.Length > 0 && (p.ItemIdentifiers.Contains(itemContext.Prefab.Identifier) || itemContext.HasTag(p.ItemIdentifiers))) || + (p.ItemComponentType != null && itemContext.Components.Any(c => c?.GetType() == p.ItemComponentType))) + { + contextualOrders.Add(p.HasOptions ? p : + new Order(p, itemContext, itemContext.Components.FirstOrDefault(c => c?.GetType() == p.ItemComponentType), Character.Controlled)); + } + } + + // If targeting a periscope connected to a turret, show the 'operateweapons' order + var orderIdentifier = "operateweapons"; + var operateWeaponsPrefab = Order.GetPrefab(orderIdentifier); + if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier)) && itemContext.Components.Any(c => c is Controller)) + { + var turret = itemContext.GetConnectedComponents().FirstOrDefault(c => operateWeaponsPrefab.ItemIdentifiers.Contains(c.Item.Prefab.Identifier)) ?? + itemContext.GetConnectedComponents(recursive: true).FirstOrDefault(c => operateWeaponsPrefab.ItemIdentifiers.Contains(c.Item.Prefab.Identifier)); + if (turret != null) { contextualOrders.Add(new Order(operateWeaponsPrefab, turret.Item, turret, Character.Controlled)); } + } + + // If targeting a repairable item, show the 'repairsystems' order + orderIdentifier = "repairsystems"; + if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier)) && itemContext.Repairables.Any()) + { + contextualOrders.Add(new Order(Order.GetPrefab(orderIdentifier), itemContext, null, Character.Controlled)); + if (itemContext.Repairables.Any(r => r != null && r.requiredSkills.Any(s => s != null && s.Identifier.Equals("electrical")))) + { + contextualOrders.Add(new Order(Order.GetPrefab("repairelectrical"), itemContext, null, Character.Controlled)); + } + else if (itemContext.Repairables.Any(r => r != null && r.requiredSkills.Any(s => s != null && s.Identifier.Equals("mechanical")))) + { + contextualOrders.Add(new Order(Order.GetPrefab("repairmechanical"), itemContext, null, Character.Controlled)); + } + } + + // Always show the 'wait' order if there are other crew members alive + orderIdentifier = "wait"; + if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier)) && characters.Any(c => c != Character.Controlled)) + { + contextualOrders.Add(new Order(Order.GetPrefab(orderIdentifier), itemContext, null, Character.Controlled)); + } + + // Remove the 'pumpwater' order if the target pump is auto-controlled (as it will immediately overwrite the work done by the bot) + orderIdentifier = "pumpwater"; + if (contextualOrders.FirstOrDefault(o => o.Identifier.Equals(orderIdentifier)) is Order o && + itemContext.Components.FirstOrDefault(c => c.GetType() == o.ItemComponentType) is Pump pump) + { + if (pump.IsAutoControlled) { contextualOrders.Remove(o); } + } + } + else if(hullContext != null) + { + contextualOrders.Add(new Order(Order.GetPrefab("fixleaks"), hullContext, null, Character.Controlled)); + } + + // Show the 'follow' and 'dismissed' orders if there are other crew members alive + if (characters.Any(c => c != Character.Controlled)) + { + var orderIdentifier = "follow"; + if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier))) + { + contextualOrders.Add(Order.GetPrefab(orderIdentifier)); + } + // Show 'dismissed' order only when there are crew members with active orders + orderIdentifier = "dismissed"; + if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier)) && + characters.Any(c => c.CurrentOrder != null && !c.CurrentOrder.Identifier.Equals(orderIdentifier))) + { + contextualOrders.Add(Order.GetPrefab(orderIdentifier)); + } + } + } + + var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, contextualOrders.Count, MathHelper.ToRadians(90f + 180f / contextualOrders.Count)); + bool disableNode = !CanSomeoneHearCharacter(); + for (int i = 0; i < contextualOrders.Count; i++) + { + optionNodes.Add(new Tuple( + CreateOrderNode(nodeSize, commandFrame.RectTransform, offsets[i].ToPoint(), contextualOrders[i], (i + 1) % 10, disableNode: disableNode, checkIfOrderCanBeHeard: false), + !disableNode ? Keys.D0 + (i + 1) % 10 : Keys.None)); + } + } + + public static bool DoesItemHaveContextualOrders(Item item) + { + if (Order.PrefabList.Any(o => o.ItemIdentifiers.Length > 0 && o.ItemIdentifiers.Contains(item.Prefab.Identifier))) { return true; } + if (Order.PrefabList.Any(o => item.HasTag(o.ItemIdentifiers))) { return true; } + if (Order.PrefabList.Any(o => o.ItemComponentType != null && item.Components.Any(c => c?.GetType() == o.ItemComponentType))) { return true; } + + if (item.Repairables.Any()) { return true; } + var operateWeaponsPrefab = Order.GetPrefab("operateweapons"); + return item.Components.Any(c => c is Controller) && + (item.GetConnectedComponents().Any(c => operateWeaponsPrefab.ItemIdentifiers.Contains(c.Item.Prefab.Identifier)) || + item.GetConnectedComponents(recursive: true).Any(c => operateWeaponsPrefab.ItemIdentifiers.Contains(c.Item.Prefab.Identifier))); + } + + private GUIButton CreateOrderNode(Point size, RectTransform parent, Point offset, Order order, int hotkey, bool disableNode = false, bool checkIfOrderCanBeHeard = true) { var node = new GUIButton( new RectTransform(size, parent: parent, anchor: Anchor.Center), style: null) @@ -1787,34 +2311,33 @@ namespace Barotrauma node.RectTransform.MoveOverTime(offset, CommandNodeAnimDuration); - var canSomeoneHearCharacter = CanSomeoneHearCharacter(); - var hasOptions = order.ItemComponentType != null || order.ItemIdentifiers.Length > 0 || order.Options.Length > 1; + if (checkIfOrderCanBeHeard && !disableNode) { disableNode = !CanSomeoneHearCharacter(); } + var mustSetOptionOrTarget = order.HasOptions || (order.MustSetTarget && itemContext == null); node.OnClicked = (button, userData) => { - if (!canSomeoneHearCharacter || !CanIssueOrders) { return false; } + if (disableNode || !CanIssueOrders) { return false; } var o = userData as Order; - // TODO: Consider defining orders' or order categories' quick-assignment possibility in the XML - if (o.Category == OrderCategory.Movement && characterContext == null) + if (o.MustManuallyAssign && characterContext == null) { CreateAssignmentNodes(node); } - else if (hasOptions) + else if (mustSetOptionOrTarget) { NavigateForward(button, userData); } else { - SetCharacterOrder(characterContext ?? GetBestCharacterForOrder(o), o, null, Character.Controlled); + SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o), o, null, Character.Controlled); DisableCommandUI(); } return true; }; var icon = CreateNodeIcon(node.RectTransform, order.SymbolSprite, order.Color, - tooltip: hasOptions || characterContext != null ? order.Name : order.Name + + tooltip: mustSetOptionOrTarget || characterContext != null ? order.Name : order.Name + "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.leftmouse") : TextManager.Get("input.rightmouse")) + ": " + TextManager.Get("commandui.quickassigntooltip") + "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.rightmouse") : TextManager.Get("input.leftmouse")) + ": " + TextManager.Get("commandui.manualassigntooltip")); - - if (!canSomeoneHearCharacter) + + if (disableNode) { node.CanBeFocused = icon.CanBeFocused = false; CreateBlockIcon(node.RectTransform); @@ -1828,25 +2351,11 @@ namespace Barotrauma private void CreateOrderOptions(Order order) { - // This is largely based on the CreateOrderTargetFrame() method - - Submarine submarine = Character.Controlled != null && Character.Controlled.TeamID == Character.TeamType.Team2 && Submarine.MainSubs.Length > 1 ? - Submarine.MainSubs[1] : - Submarine.MainSub; - - List matchingItems = new List(); - if (order.ItemComponentType != null || order.ItemIdentifiers.Length > 0) - { - matchingItems = order.ItemIdentifiers.Length > 0 ? - Item.ItemList.FindAll(it => order.ItemIdentifiers.Contains(it.Prefab.Identifier) || it.HasTag(order.ItemIdentifiers)) : - Item.ItemList.FindAll(it => it.Components.Any(ic => ic.GetType() == order.ItemComponentType)); - - matchingItems.RemoveAll(it => it.Submarine != submarine && !submarine.DockedTo.Contains(it.Submarine)); - matchingItems.RemoveAll(it => it.Submarine != null && it.Submarine.Info.Type != SubmarineInfo.SubmarineType.Player); - } + Submarine submarine = GetTargetSubmarine(); + var matchingItems = (itemContext == null && order.MustSetTarget) ? order.GetMatchingItems(submarine, true) : new List(); //more than one target item -> create a minimap-like selection with a pic of the sub - if (matchingItems.Count > 1) + if (itemContext == null && matchingItems.Count > 1) { // TODO: Further adjustments to frameSize calculations // I just divided the existing sizes by 2 to get it working quickly without it overlapping too much @@ -1882,7 +2391,7 @@ namespace Barotrauma UserData = submarine }; - List optionFrames = new List(); + List optionElements = new List(); foreach (Item item in matchingItems) { var itemTargetFrame = targetFrame.Children.First().FindChild(item); @@ -1903,52 +2412,87 @@ namespace Barotrauma anchor = Anchor.TopRight; } - var optionFrame = new GUIFrame( - new RectTransform( - new Point((int)(250 * GUI.Scale), (int)((40 + order.Options.Length * 40) * GUI.Scale)), - parent: itemTargetFrame.RectTransform, - anchor: anchor), - style: "InnerFrame"); - - new GUIFrame( - new RectTransform(Vector2.One, optionFrame.RectTransform, anchor: Anchor.Center), - style: "OuterGlow", - color: Color.Black * 0.7f); - - var optionContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.9f), optionFrame.RectTransform, anchor: Anchor.Center)) + GUIComponent optionElement; + if (order.Options.Length > 1) { - RelativeSpacing = 0.05f, - Stretch = true - }; + optionElement = new GUIFrame( + new RectTransform( + new Point((int)(250 * GUI.Scale), (int)((40 + order.Options.Length * 40) * GUI.Scale)), + parent: itemTargetFrame.RectTransform, + anchor: anchor), + style: "InnerFrame"); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), optionContainer.RectTransform), item != null ? item.Name : order.Name); + new GUIFrame( + new RectTransform(Vector2.One, optionElement.RectTransform, anchor: Anchor.Center), + style: "OuterGlow", + color: Color.Black * 0.7f); - for (int i = 0; i < order.Options.Length; i++) - { - optionNodes.Add(new Tuple( - new GUIButton( - new RectTransform(new Vector2(1.0f, 0.2f), optionContainer.RectTransform), - text: order.OptionNames[i], - style: "GUITextBox") - { - UserData = new Tuple( - item == null ? order : new Order(order, item, item.Components.FirstOrDefault(ic => ic.GetType() == order.ItemComponentType)), - order.Options[i]), - Font = GUI.SmallFont, - OnClicked = (_, userData) => + var optionContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.9f), optionElement.RectTransform, anchor: Anchor.Center)) + { + RelativeSpacing = 0.05f, + Stretch = true + }; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), optionContainer.RectTransform), item != null ? item.Name : order.Name); + + for (int i = 0; i < order.Options.Length; i++) + { + optionNodes.Add(new Tuple( + new GUIButton( + new RectTransform(new Vector2(1.0f, 0.2f), optionContainer.RectTransform), + text: order.GetOptionName(i), + style: "GUITextBox") { - if (!CanIssueOrders) { return false; } - var o = userData as Tuple; - SetCharacterOrder(characterContext ?? GetBestCharacterForOrder(o.Item1), o.Item1, o.Item2, Character.Controlled); - DisableCommandUI(); - return true; - } - }, - Keys.None)); + UserData = new Tuple( + item == null ? order : new Order(order, item, item.Components.FirstOrDefault(ic => ic.GetType() == order.ItemComponentType)), + order.Options[i]), + Font = GUI.SmallFont, + OnClicked = (_, userData) => + { + if (!CanIssueOrders) { return false; } + var o = userData as Tuple; + SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o.Item1), o.Item1, o.Item2, Character.Controlled); + DisableCommandUI(); + return true; + } + }, + Keys.None)); + } } - optionFrames.Add(optionFrame); + else + { + var userData = new Tuple(item == null ? order : new Order(order, item, item.Components.FirstOrDefault(ic => ic.GetType() == order.ItemComponentType)), ""); + optionElement = new GUIButton( + new RectTransform( + new Point((int)(50 * GUI.Scale)), + parent: itemTargetFrame.RectTransform, + anchor: anchor), + style: null) + { + UserData = userData, + Font = GUI.SmallFont, + ToolTip = item?.Name ?? order.Name, + OnClicked = (_, userData) => + { + if (!CanIssueOrders) { return false; } + var o = userData as Tuple; + SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o.Item1), o.Item1, o.Item2, Character.Controlled); + DisableCommandUI(); + return true; + } + }; + + Sprite icon = null; + order.MinimapIcons?.TryGetValue(item.Prefab.Identifier, out icon); + var colorMultiplier = characters.Any(c => c.CurrentOrder != null && + c.CurrentOrder.Identifier == userData.Item1.Identifier && + c.CurrentOrder.TargetEntity == userData.Item1.TargetEntity) ? 0.5f : 1f; + CreateNodeIcon(optionElement.RectTransform, icon ?? order.SymbolSprite, order.Color * colorMultiplier); + optionNodes.Add(new Tuple(optionElement, Keys.None)); + } + optionElements.Add(optionElement); } - GUI.PreventElementOverlap(optionFrames, clampArea: new Rectangle(10, 10, GameMain.GraphicsWidth - 20, GameMain.GraphicsHeight - 20)); + GUI.PreventElementOverlap(optionElements, clampArea: new Rectangle(10, 10, GameMain.GraphicsWidth - 20, GameMain.GraphicsHeight - 20)); var shadow = new GUIFrame( new RectTransform(targetFrame.Rect.Size + new Point((int)(200 * GUI.Scale)), targetFrame.RectTransform, anchor: Anchor.Center), @@ -1959,8 +2503,10 @@ namespace Barotrauma //only one target (or an order with no particular targets), just show options else { - var item = matchingItems.Count > 0 ? matchingItems[0] : null; - var o = item == null ? order : new Order(order, item, item.Components.FirstOrDefault(ic => ic.GetType() == order.ItemComponentType)); + var item = itemContext != null ? + (order.UseController ? itemContext.GetConnectedComponents().FirstOrDefault()?.Item ?? itemContext.GetConnectedComponents(recursive: true).FirstOrDefault()?.Item : itemContext) : + (matchingItems.Count > 0 ? matchingItems[0] : null); + var o = item == null || !order.IsPrefab ? order : new Order(order, item, item.Components.FirstOrDefault(ic => ic.GetType() == order.ItemComponentType)); var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, GetCircumferencePointCount(order.Options.Length), GetFirstNodeAngle(order.Options.Length)); @@ -1968,7 +2514,7 @@ namespace Barotrauma for (int i = 0; i < order.Options.Length; i++) { optionNodes.Add(new Tuple( - CreateOrderOptionNode(nodeSize, commandFrame.RectTransform, offsets[offsetIndex++].ToPoint(), o, order.Options[i], order.OptionNames[i], (i + 1) % 10), + CreateOrderOptionNode(nodeSize, commandFrame.RectTransform, offsets[offsetIndex++].ToPoint(), o, order.Options[i], order.GetOptionName(i), (i + 1) % 10), Keys.D0 + (i + 1) % 10)); } } @@ -1976,23 +2522,20 @@ namespace Barotrauma private GUIButton CreateOrderOptionNode(Point size, RectTransform parent, Point offset, Order order, string option, string optionName, int hotkey) { - var node = new GUIButton( - new RectTransform(size, parent: parent, anchor: Anchor.Center) - { - AbsoluteOffset = offset - }, - style: null) + var node = new GUIButton(new RectTransform(size, parent: parent, anchor: Anchor.Center), style: null) { UserData = new Tuple(order, option), OnClicked = (_, userData) => { if (!CanIssueOrders) { return false; } var o = userData as Tuple; - SetCharacterOrder(characterContext ?? GetBestCharacterForOrder(o.Item1), o.Item1, o.Item2, Character.Controlled); + SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o.Item1), o.Item1, o.Item2, Character.Controlled); DisableCommandUI(); return true; } }; + node.RectTransform.MoveOverTime(offset, CommandNodeAnimDuration); + GUIImage icon = null; if (order.Prefab.OptionSprites.TryGetValue(option, out Sprite sprite)) { @@ -2019,41 +2562,48 @@ namespace Barotrauma var order = (node.UserData is Order) ? new Tuple(node.UserData as Order, null) : node.UserData as Tuple; - var characters = GetCharactersSortedForOrder(order.Item1); - if (characters.Count < 1) { return; } + var characters = GetCharactersForManualAssignment(order.Item1); + if (characters.None()) { return; } if (!(optionNodes.Find(n => n.Item1 == node) is Tuple optionNode) || !optionNodes.Remove(optionNode)) { shortcutNodes.Remove(node); }; RemoveOptionNodes(); + if (returnNode != null) { returnNode.Children.ForEach(child => child.Visible = false); returnNode.Visible = false; historyNodes.Push(returnNode); } - SetReturnNode(centerNode, new Point( - (int)(node.RectTransform.AbsoluteOffset.X * -returnNodeDistanceModifier), - (int)(node.RectTransform.AbsoluteOffset.Y * -returnNodeDistanceModifier))); + SetReturnNode(centerNode, new Point(0, (int)(returnNodeDistanceModifier * nodeDistance))); + if (targetFrame == null || !targetFrame.Visible) { SetCenterNode(node as GUIButton); } else { - var clickedOptionNode = new GUIButton( + if (string.IsNullOrEmpty(order.Item2)) + { + SetCenterNode(node as GUIButton, resetAnchor: true); + } + else + { + var clickedOptionNode = new GUIButton( new RectTransform(centerNodeSize, parent: commandFrame.RectTransform, anchor: Anchor.Center), style: null) - { - UserData = node.UserData - }; - if (order.Item1.Prefab.OptionSprites.TryGetValue(order.Item2, out Sprite sprite)) - { - CreateNodeIcon(clickedOptionNode.RectTransform, sprite, order.Item1.Color, tooltip: order.Item2); + { + UserData = node.UserData + }; + if (order.Item1.Prefab.OptionSprites.TryGetValue(order.Item2, out Sprite sprite)) + { + CreateNodeIcon(clickedOptionNode.RectTransform, sprite, order.Item1.Color, tooltip: order.Item2); + } + SetCenterNode(clickedOptionNode); + node = null; } - SetCenterNode(clickedOptionNode); - node = null; targetFrame.Visible = false; } if (shortcutCenterNode != null) @@ -2062,21 +2612,36 @@ namespace Barotrauma shortcutCenterNode = null; } - var needToExpand = characters.Count > assignmentNodeMaxCount + 1; - var nodeCount = needToExpand ? assignmentNodeMaxCount + 1 : characters.Count; - var extraNodeDistance = Math.Max(nodeCount - 6, 0) * (GUI.Scale * 30); - var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance + extraNodeDistance, - GetCircumferencePointCount(nodeCount), - GetFirstNodeAngle(nodeCount)); - - var i = 0; - var assignmentNodeCount = (needToExpand ? nodeCount - 1 : nodeCount); - for (; i < assignmentNodeCount; i++) + var characterCount = characters.Count; + int hotkey = 1; + Vector2[] offsets; + var needToExpand = characterCount > 10; + if (characterCount > 5) { - CreateAssignmentNode(order, characters[i], offsets[i].ToPoint(), (i + 1) % 10); + // First ring + var charactersOnFirstRing = needToExpand ? 5 : (int)Math.Floor(characterCount / 2f); + offsets = GetAssignmentNodeOffsets(charactersOnFirstRing); + for (int i = 0; i < charactersOnFirstRing; i++) + { + CreateAssignmentNode(order, characters[i], offsets[i].ToPoint(), hotkey++ % 10); + } + // Second ring + var charactersOnSecondRing = needToExpand ? 4 : characterCount - charactersOnFirstRing; + offsets = GetAssignmentNodeOffsets(needToExpand ? 5 : charactersOnSecondRing, false); + for (int i = 0; i < charactersOnSecondRing; i++) + { + CreateAssignmentNode(order, characters[charactersOnFirstRing + i], offsets[i].ToPoint(), hotkey++ % 10); + } + } + else + { + offsets = GetAssignmentNodeOffsets(characterCount); + for (int i = 0; i < characterCount; i++) + { + CreateAssignmentNode(order, characters[i], offsets[i].ToPoint(), hotkey++ % 10); + } } - int hotkey; if (!needToExpand) { hotkey = optionNodes.Count + 1; @@ -2087,12 +2652,14 @@ namespace Barotrauma } extraOptionCharacters.Clear(); - extraOptionCharacters.AddRange(characters.GetRange(i, characters.Count - i)); + // Sort expanded assignment nodes by characters' jobs and then by their names + extraOptionCharacters.AddRange(characters.GetRange(hotkey - 1, characterCount - (hotkey - 1)) + .OrderBy(c => c?.Info?.Job?.Name).ThenBy(c => c?.Info?.DisplayName)); expandNode = new GUIButton( - new RectTransform(nodeSize, parent: commandFrame.RectTransform, anchor: Anchor.Center) + new RectTransform(assignmentNodeSize, parent: commandFrame.RectTransform, anchor: Anchor.Center) { - AbsoluteOffset = offsets[i].ToPoint() + AbsoluteOffset = offsets.Last().ToPoint() }, style: null) { @@ -2108,6 +2675,22 @@ namespace Barotrauma returnNodeHotkey = Keys.D0 + hotkey % 10; } + private Vector2[] GetAssignmentNodeOffsets(int characters, bool firstRing = true) + { + var nodeDistance = 1.8f * this.nodeDistance; + var nodePositionsOnEachSide = characters % 2 > 0 ? 7 : 6; + var nodeCountForCalculation = 2 * nodePositionsOnEachSide + 2; + var offsets = MathUtils.GetPointsOnCircumference(firstRing ? new Vector2(0f, 0.5f * nodeDistance) : Vector2.Zero, + nodeDistance, nodeCountForCalculation, MathHelper.ToRadians(180f + 360f / nodeCountForCalculation)); + var emptySpacesPerSide = (nodePositionsOnEachSide - characters) / 2; + var offsetsInUse = new Vector2[nodePositionsOnEachSide - 2 * emptySpacesPerSide]; + for (int i = 0; i < offsetsInUse.Length; i++) + { + offsetsInUse[i] = offsets[i + emptySpacesPerSide]; + } + return offsetsInUse; + } + private bool ExpandAssignmentNodes(GUIButton node, object userData) { node.OnClicked = (button, _) => @@ -2117,57 +2700,107 @@ namespace Barotrauma return true; }; - var order = userData as Tuple; - // TODO: The value 100 should be determined by how large the inner circle is - var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, (nodeDistance + GUI.Scale * 100) * 1.55f, - GetCircumferencePointCount(extraOptionCharacters.Count), - GetFirstNodeAngle(extraOptionCharacters.Count)); + var availableNodePositions = 20; + var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, 2.7f * this.nodeDistance, availableNodePositions, + firstAngle: MathHelper.ToRadians(-90f - ((extraOptionCharacters.Count - 1) * 0.5f * (360f / availableNodePositions)))); for (int i = 0; i < extraOptionCharacters.Count; i++) { - CreateAssignmentNode(order, extraOptionCharacters[i], offsets[i].ToPoint(), -1); + CreateAssignmentNode(userData as Tuple, extraOptionCharacters[i], offsets[i].ToPoint(), -1, nameLabelScale: 1.15f); } return true; } - private void CreateAssignmentNode(Tuple order, Character character, Point offset, int hotkey) + private void CreateAssignmentNode(Tuple order, Character character, Point offset, int hotkey, float nameLabelScale = 1f) { // Button var node = new GUIButton( - new RectTransform(nodeSize, parent: commandFrame.RectTransform, anchor: Anchor.Center), + new RectTransform(assignmentNodeSize, parent: commandFrame.RectTransform, anchor: Anchor.Center), style: null) { - OnClicked = (button, userData) => + UserData = character, + OnClicked = (_, userData) => { if (!CanIssueOrders) { return false; } - SetCharacterOrder(character, order.Item1, order.Item2, Character.Controlled); + SetCharacterOrder(userData as Character, order.Item1, order.Item2, Character.Controlled); DisableCommandUI(); return true; } }; node.RectTransform.MoveOverTime(offset, CommandNodeAnimDuration); - // Container - var icon = new GUIImage( - new RectTransform(new Vector2(1.2f), node.RectTransform, anchor: Anchor.Center), - "CommandNodeContainer", - scaleToFit: true) + + var jobColor = character.Info?.Job?.Prefab?.UIColor ?? Color.White; + + // Order icon + GUIImage orderIcon; + if (character.CurrentOrder != null && !character.CurrentOrder.Identifier.Equals("dismissed")) { - Color = character.Info.Job.Prefab.UIColor * nodeColorMultiplier, - HoverColor = character.Info.Job.Prefab.UIColor, - PressedColor = character.Info.Job.Prefab.UIColor, - SelectedColor = character.Info.Job.Prefab.UIColor, - UserData = "colorsource" - }; - // Character icon - new GUICustomComponent( - new RectTransform(Vector2.One, node.RectTransform), - (spriteBatch, _) => + orderIcon = new GUIImage(new RectTransform(new Vector2(1.2f), node.RectTransform, anchor: Anchor.Center), + character.CurrentOrder.SymbolSprite, scaleToFit: true); + var tooltip = character.CurrentOrder.Name; + if (!string.IsNullOrWhiteSpace(character.CurrentOrderOption)) { tooltip += " (" + character.CurrentOrder.GetOptionName(character.CurrentOrderOption) + ")"; }; + orderIcon.ToolTip = tooltip; + } + else + { + // TODO: Replace with an icon that symbols the characters dismissed state and their availability to new orders OR localize the text + orderIcon = new GUIImage(new RectTransform(new Vector2(1.2f), node.RectTransform, anchor: Anchor.Center), + "CommandNodeContainer", scaleToFit: true); + var label = new GUITextBlock(new RectTransform(new Vector2(0.9f / 1.2f), orderIcon.RectTransform, anchor: Anchor.Center), + "FREE", textColor: jobColor * nodeColorMultiplier, font: GUI.SubHeadingFont, textAlignment: Alignment.Center, style: null) { - character.Info.DrawIcon(spriteBatch, node.Center, node.Rect.Size.ToVector2() * 0.75f); - }) + CanBeFocused = false, + ForceUpperCase = true, + HoverTextColor = jobColor + }; + } + orderIcon.Color = jobColor * nodeColorMultiplier; + orderIcon.HoverColor = jobColor; + orderIcon.PressedColor = jobColor; + orderIcon.SelectedColor = jobColor; + orderIcon.UserData = "colorsource"; + + // Name label + var width = (int)(nameLabelScale * nodeSize.X); + var font = GUI.SmallFont; + var nameLabel = new GUITextBlock( + new RectTransform(new Point(width, 0), parent: node.RectTransform, anchor: Anchor.TopCenter, pivot: Pivot.BottomCenter) + { + RelativeOffset = new Vector2(0f, -0.1f) + }, + ToolBox.LimitString(character.Info?.DisplayName, font, width), textColor: jobColor * nodeColorMultiplier, font: font, textAlignment: Alignment.Center, style: null) { - ToolTip = character.Info.DisplayName + " (" + character.Info.Job.Name + ")" + CanBeFocused = false, + ForceUpperCase = true, + HoverTextColor = jobColor }; + // Job icon + GUIImage jobIcon = null; + if (character?.Info?.Job?.Prefab?.Icon is Sprite sprite) + { + jobIcon = new GUIImage(new RectTransform(new Vector2(1.2f), node.RectTransform, anchor: Anchor.Center), + "CommandNodeContainer", scaleToFit: true) + { + CanBeFocused = false, + Color = jobColor * nodeColorMultiplier, + HoverColor = jobColor, + PressedColor = jobColor, + SelectedColor = jobColor, + Visible = false + }; ; + new GUIImage(new RectTransform(new Vector2(0.9f / 1.2f), jobIcon.RectTransform, anchor: Anchor.Center), + sprite, scaleToFit: true) + { + CanBeFocused = false, + Color = jobColor * nodeColorMultiplier, + HoverColor = jobColor, + PressedColor = jobColor, + SelectedColor = jobColor + }; + } + + assignmentNodeIcons.Add(new AssignmentNodeIconSet(orderIcon, jobIcon)); + #if DEBUG bool canHear = true; #else @@ -2175,7 +2808,7 @@ namespace Barotrauma #endif if (!canHear) { - node.CanBeFocused = icon.CanBeFocused = false; + node.CanBeFocused = orderIcon.CanBeFocused = false; CreateBlockIcon(node.RectTransform); } if (hotkey >= 0) @@ -2250,8 +2883,9 @@ namespace Barotrauma private void CreateBlockIcon(RectTransform parent) { - new GUIImage(new RectTransform(Vector2.One, parent, anchor: Anchor.Center), cancelIcon, scaleToFit: true) + new GUIImage(new RectTransform(new Vector2(0.9f), parent, anchor: Anchor.Center), cancelIcon, scaleToFit: true) { + CanBeFocused = false, Color = GUI.Style.Red * nodeColorMultiplier, HoverColor = GUI.Style.Red }; @@ -2291,9 +2925,48 @@ namespace Barotrauma return (degrees < 0) ? (degrees + 360) : degrees; } + private bool TryGetBreachedHullAtHoveredWall(out Hull breachedHull) + { + breachedHull = null; + // Based on the IsValidTarget() method of AIObjectiveFixLeaks class + List leaks = Gap.GapList.FindAll(g => + g != null && g.ConnectedWall != null && g.ConnectedDoor == null && g.Open > 0 && g.linkedTo.Any(l => l != null) && + g.Submarine != null && (Character.Controlled != null && g.Submarine.TeamID == Character.Controlled.TeamID && g.Submarine.Info.IsPlayer)); + if (leaks.None()) { return false; } + Vector2 mouseWorldPosition = GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition); + foreach (Gap leak in leaks) + { + if (Submarine.RectContains(leak.ConnectedWall.WorldRect, mouseWorldPosition)) + { + breachedHull = leak.FlowTargetHull; + return true; + } + } + return false; + } + + private Submarine GetTargetSubmarine() + { + var sub = Submarine.MainSub; + if (Character.Controlled != null) + { + // Pick the second main sub when we have two teams (in combat mission) + if (Character.Controlled.TeamID == Character.TeamType.Team2 && Submarine.MainSubs.Length > 1) + { + sub = Submarine.MainSubs[1]; + } + // Target current submarine (likely a shuttle) when undocked from the main sub + if (Character.Controlled.Submarine is Submarine currentSub && currentSub != sub && currentSub.TeamID == Character.Controlled.TeamID && !currentSub.IsConnectedTo(sub)) + { + sub = currentSub; + } + } + return sub; + } + #region Crew Member Assignment Logic - private Character GetBestCharacterForOrder(Order order) + private Character GetCharacterForQuickAssignment(Order order) { #if !DEBUG if (Character.Controlled == null) { return null; } @@ -2302,123 +2975,37 @@ namespace Barotrauma { return operatingCharacter; } - return characters.FindAll(c => Character.Controlled == null || (c != Character.Controlled && c.TeamID == Character.Controlled.TeamID)) - .OrderByDescending(c => c.CurrentOrder == null || c.CurrentOrder.Identifier == dismissedOrderPrefab.Identifier) - .ThenByDescending(c => order.HasAppropriateJob(c)) - .ThenBy(c => c.CurrentOrder?.Weight) - .FirstOrDefault(); + return GetCharactersSortedForOrder(order, false).FirstOrDefault() ?? Character.Controlled; } - private List GetCharactersSortedForOrder(Order order) + private List GetCharactersForManualAssignment(Order order) { #if !DEBUG if (Character.Controlled == null) { return new List(); } #endif - if (order.Identifier == "follow") + if (order.Identifier == dismissedOrderPrefab.Identifier) { - return characters.FindAll(c => Character.Controlled == null || (c != Character.Controlled && c.TeamID == Character.Controlled.TeamID)) - .OrderByDescending(c => c.CurrentOrder == null || c.CurrentOrder.Identifier == dismissedOrderPrefab.Identifier) - .ToList(); + return characters.FindAll(c => c.CurrentOrder != null && c.CurrentOrder.Identifier != dismissedOrderPrefab.Identifier) + .OrderBy(c => c.Info.DisplayName).ToList(); } - else - { - return characters.FindAll(c => Character.Controlled == null || c.TeamID == Character.Controlled.TeamID) - .OrderByDescending(c => c.CurrentOrder == null || c.CurrentOrder.Identifier == dismissedOrderPrefab.Identifier) + return GetCharactersSortedForOrder(order, order.Identifier != "follow").ToList(); + } + + private IEnumerable GetCharactersSortedForOrder(Order order, bool includeSelf) + { + return characters.FindAll(c => Character.Controlled == null || ((includeSelf || c != Character.Controlled) && c.TeamID == Character.Controlled.TeamID)) + .OrderByDescending(c => c.CurrentOrder != null && order.Category == OrderCategory.Operate && c.CurrentOrder.Identifier == order.Identifier && c.CurrentOrder.TargetEntity == order.TargetEntity) + .ThenByDescending(c => c.CurrentOrder == null || c.CurrentOrder.Identifier == dismissedOrderPrefab.Identifier) + .ThenBy(c => c.CurrentOrder != null && c.CurrentOrder.Identifier == order.Identifier && c.CurrentOrder.TargetEntity == order.TargetEntity) .ThenByDescending(c => order.HasAppropriateJob(c)) .ThenBy(c => c.CurrentOrder?.Weight) - .ToList(); - } + .ThenByDescending(c => c.GetSkillLevel(order.AppropriateSkill)); } #endregion #endregion - /// - /// Creates a listbox that includes all the characters in the crew, can be used externally (round info menus etc) - /// - public void CreateCrewListFrame(IEnumerable crew, GUIFrame crewFrame) - { - List teamIDs = crew.Select(c => c.TeamID).Distinct().ToList(); - - if (!teamIDs.Any()) teamIDs.Add(Character.TeamType.None); - - int listBoxHeight = 300 / teamIDs.Count; - - var content = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), crewFrame.RectTransform)) - { - Stretch = true - }; - - for (int i = 0; i < teamIDs.Count; i++) - { - if (teamIDs.Count > 1) - { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), content.RectTransform), CombatMission.GetTeamName(teamIDs[i])); - } - - GUIListBox crewList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.1f) }) - { - UserData = crewFrame - }; - crewList.OnSelected = (component, obj) => - { - SelectCrewCharacter(component.UserData as Character, crewList); - return true; - }; - - foreach (Character character in crew.Where(c => c.TeamID == teamIDs[i])) - { - GUIFrame frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.15f), crewList.Content.RectTransform), style: "ListBoxElement") - { - UserData = character, - Color = (GameMain.NetworkMember != null && GameMain.Client.Character == character) ? Color.Gold * 0.2f : Color.Transparent - }; - - var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: true) - { - RelativeSpacing = 0.05f, - Stretch = true - }; - - new GUICustomComponent(new RectTransform(new Vector2(0.2f, 1.0f), paddedFrame.RectTransform, Anchor.CenterLeft), - onDraw: (sb, component) => character.Info.DrawIcon(sb, component.Rect.Center.ToVector2(), targetAreaSize: component.Rect.Size.ToVector2())) - { - CanBeFocused = false, - HoverColor = Color.White, - SelectedColor = Color.White - }; - - GUITextBlock textBlock = new GUITextBlock(new RectTransform(Vector2.One, paddedFrame.RectTransform), - ToolBox.LimitString(character.Info.Name + " (" + character.Info.Job.Name + ")", GUI.Font, paddedFrame.Rect.Width - paddedFrame.Rect.Height), - textColor: character.Info.Job.Prefab.UIColor); - } - } - } - - /// - /// Select a character from CrewListFrame - /// - protected bool SelectCrewCharacter(Character character, GUIComponent crewList) - { - if (character == null) { return false; } - - GUIComponent crewFrame = (GUIComponent)crewList.UserData; - GUIComponent existingPreview = crewFrame.FindChild("SelectedCharacter"); - if (existingPreview != null) { crewFrame.RemoveChild(existingPreview); } - - var previewPlayer = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.7f), crewFrame.RectTransform, Anchor.TopRight), style: null) - { - UserData = "SelectedCharacter" - }; - - character.Info.CreateInfoFrame(previewPlayer); - - if (GameMain.NetworkMember != null) { GameMain.Client.SelectCrewCharacter(character, previewPlayer); } - - return true; - } - #region Reports /// @@ -2430,22 +3017,21 @@ namespace Barotrauma if (Character.Controlled?.CurrentHull?.Submarine != null && Character.Controlled.SpeechImpediment < 100.0f) { WifiComponent radio = GetHeadset(Character.Controlled, true); - canIssueOrders = radio != null && radio.CanTransmit(); + canIssueOrders = + radio != null && + radio.CanTransmit() && + Character.Controlled?.CurrentHull?.Submarine?.TeamID == Character.Controlled.TeamID && + !Character.Controlled.CurrentHull.Submarine.Info.IsWreck; } if (canIssueOrders) { - //report buttons are hidden when accessing another character's inventory - ReportButtonFrame.Visible = !Character.Controlled.ShouldLockHud() && - (Character.Controlled?.SelectedCharacter?.Inventory == null || - !Character.Controlled.SelectedCharacter.CanInventoryBeAccessed); + ReportButtonFrame.Visible = !Character.Controlled.ShouldLockHud(); + if (!ReportButtonFrame.Visible) { return; } var reportButtonParent = ChatBox ?? GameMain.Client?.ChatBox; if (reportButtonParent == null) { return; } - var sub = Character.Controlled.Submarine; - if (sub == null || sub.TeamID != Character.Controlled.TeamID || sub.Info.IsWreck) { return; } - ReportButtonFrame.RectTransform.AbsoluteOffset = new Point(reportButtonParent.GUIFrame.Rect.Right + (int)(10 * GUI.Scale), reportButtonParent.GUIFrame.Rect.Y); bool hasFires = Character.Controlled.CurrentHull.FireSources.Count > 0; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index e73d93e9c..06ba7d188 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -134,7 +134,7 @@ namespace Barotrauma foreach (PurchasedItem pi in CargoManager.PurchasedItems) { msg.Write(pi.ItemPrefab.Identifier); - msg.Write((UInt16)pi.Quantity); + msg.WriteRangedInteger(pi.Quantity, 0, 100); } } @@ -162,7 +162,7 @@ namespace Barotrauma for (int i = 0; i < purchasedItemCount; i++) { string itemPrefabIdentifier = msg.ReadString(); - UInt16 itemQuantity = msg.ReadUInt16(); + int itemQuantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity); purchasedItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity)); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index f9a62b2bb..f61c1174a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -25,7 +25,7 @@ namespace Barotrauma : base(preset, param) { int buttonHeight = (int)(HUDLayoutSettings.ButtonAreaTop.Height * 0.7f); - endRoundButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle(HUDLayoutSettings.ButtonAreaTop.Right - 200, HUDLayoutSettings.ButtonAreaTop.Center.Y - buttonHeight / 2, 200, buttonHeight), GUICanvas.Instance), + endRoundButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle(HUDLayoutSettings.ButtonAreaTop.Right - GUI.IntScale(200), HUDLayoutSettings.ButtonAreaTop.Center.Y - buttonHeight / 2, GUI.IntScale(200), buttonHeight), GUICanvas.Instance), TextManager.Get("EndRound"), textAlignment: Alignment.Center) { Font = GUI.SmallFont, @@ -276,6 +276,7 @@ namespace Barotrauma c.SaveInventory(c.Inventory, inventoryElement); c.Info.InventoryData = inventoryElement; c.Inventory?.DeleteAllItems(); + c.ResetCurrentOrder(); } GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs index 89c5fbf6c..8825ec280 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs @@ -246,7 +246,7 @@ namespace Barotrauma.Tutorials { //captain_navConsoleCustomInterface.HighlightElement(0, uiHighlightColor, duration: 1.0f, pulsateAmount: 0.0f); yield return new WaitForSeconds(1.0f, false); - } while (!Submarine.MainSub.AtEndPosition || Submarine.MainSub.DockedTo.Any()); + } while (!Submarine.MainSub.AtEndPosition || !Submarine.MainSub.DockedTo.Any()); RemoveCompletedObjective(segments[6]); yield return new WaitForSeconds(3f, false); GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.GetWithVariable("Captain.Radio.Complete", "[OUTPOSTNAME]", GameMain.GameSession.EndLocation.Name), ChatMessageType.Radio, null); @@ -284,7 +284,9 @@ namespace Barotrauma.Tutorials private bool IsSelectedItem(Item item) { - return captain?.SelectedConstruction == item; + return + captain?.SelectedConstruction == item || + (captain?.SelectedConstruction?.linkedTo?.Contains(item) ?? false); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs index 28999e871..d4717ab96 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs @@ -156,7 +156,7 @@ namespace Barotrauma.Tutorials //yield return new WaitForSeconds(2.5f); doctor.SetStun(1.5f); - var explosion = new Explosion(range: 100, force: 10, damage: 0, structureDamage: 0); + var explosion = new Explosion(range: 100, force: 10, damage: 0, structureDamage: 0, itemDamage: 0); explosion.DisableParticles(); GameMain.GameScreen.Cam.Shake = shakeAmount; explosion.Explode(Character.Controlled.WorldPosition - Vector2.UnitX * 25, null); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs index b11083859..0585ae807 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/OfficerTutorial.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Xml.Linq; using System.Linq; using Barotrauma.Items.Components; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs index d659ef6ea..e82aef15e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs @@ -2,7 +2,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs index bbd67e90e..8127788ae 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs @@ -515,7 +515,7 @@ namespace Barotrauma.Tutorials height += (int)GUI.Font.MeasureString(title).Y + (int)(150 * GUI.Scale); } - var background = new GUIFrame(new RectTransform(new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight), GUI.Canvas, Anchor.Center), style: null, Color.Black * 0.5f); + var background = new GUIFrame(new RectTransform(new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight), GUI.Canvas, Anchor.Center), style: "GUIBackgroundBlocker"); var infoBlock = new GUIFrame(new RectTransform(new Point(width, height), background.RectTransform, anchor)); infoBlock.Flash(GUI.Style.Green); @@ -533,15 +533,15 @@ namespace Barotrauma.Tutorials titleBlock.RectTransform.IsFixedSize = true; } - List colorData = ColorData.GetColorData(text, out text); + List richTextData = RichTextData.GetRichTextData(text, out text); GUITextBlock textBlock; - if (colorData == null) + if (richTextData == null) { textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), " " + text, wrap: true); } else { - textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), colorData, " " + text, wrap: true); + textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), richTextData, " " + text, wrap: true); } textBlock.RectTransform.IsFixedSize = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs index 903f55d0f..38e4dff6d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs @@ -1,159 +1,41 @@ -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; -using System.Collections.Generic; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; namespace Barotrauma { partial class GameSession { - private InfoFrameTab selectedTab; - private GUIFrame infoFrame; - - private readonly List tabButtons = new List(); - - private GUIFrame infoFrameContent; public RoundSummary RoundSummary { get; private set; } - public static bool IsInfoFrameOpen => GameMain.GameSession?.infoFrame != null; + public static bool IsTabMenuOpen => GameMain.GameSession?.tabMenu != null; + public static TabMenu TabMenuInstance => GameMain.GameSession?.tabMenu; - private bool ToggleInfoFrame() + private TabMenu tabMenu; + + public bool ToggleTabMenu() { 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) + if (tabMenu == null && GameMode is TutorialMode == false) { - CreateInfoFrame(); - SelectInfoFrameTab(null, selectedTab); + tabMenu = new TabMenu(); } else { - infoFrame = null; + tabMenu = null; + NetLobbyScreen.JobInfoFrame = null; } return true; } - public void CreateInfoFrame() - { - int width = 600, height = 400; - - tabButtons.Clear(); - - infoFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), style: "GUIBackgroundBlocker"); - - 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) - { - RelativeSpacing = 0.01f - }; - infoFrameContent = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.85f), paddedFrame.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.08f) }, style: "InnerFrame"); - - var crewButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), buttonArea.RectTransform), TextManager.Get("Crew"), style: "GUITabButton") - { - UserData = InfoFrameTab.Crew, - OnClicked = SelectInfoFrameTab - }; - tabButtons.Add(crewButton); - - var missionButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), buttonArea.RectTransform), TextManager.Get("Mission"), style: "GUITabButton") - { - UserData = InfoFrameTab.Mission, - OnClicked = SelectInfoFrameTab - }; - tabButtons.Add(missionButton); - - if (GameMain.NetworkMember != null) - { - var myCharacterButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), buttonArea.RectTransform), TextManager.Get("MyCharacter"), style: "GUITabButton") - { - UserData = InfoFrameTab.MyCharacter, - OnClicked = SelectInfoFrameTab - }; - tabButtons.Add(myCharacterButton); - } - - /*TODO: fix - if (GameMain.Server != null) - { - var manageButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), buttonArea.RectTransform), TextManager.Get("ManagePlayers")) - { - UserData = InfoFrameTab.ManagePlayers, - OnClicked = SelectInfoFrameTab - }; - }*/ - - } - - private bool SelectInfoFrameTab(GUIButton button, object userData) - { - selectedTab = (InfoFrameTab)userData; - - CreateInfoFrame(); - tabButtons.ForEach(tb => tb.Selected = (InfoFrameTab)tb.UserData == selectedTab); - - switch (selectedTab) - { - case InfoFrameTab.Crew: - CrewManager.CreateCrewListFrame(CrewManager.GetCharacters(), infoFrameContent); - break; - case InfoFrameTab.Mission: - CreateMissionInfo(infoFrameContent); - break; - case InfoFrameTab.MyCharacter: - if (GameMain.NetworkMember == null) { return false; } - GameMain.NetLobbyScreen.CreatePlayerFrame(infoFrameContent); - break; - case InfoFrameTab.ManagePlayers: - //TODO: fix - //GameMain.Server.ManagePlayersFrame(infoFrameContent); - break; - } - - return true; - } - - private void CreateMissionInfo(GUIFrame infoFrame) - { - infoFrameContent.ClearChildren(); - - var isTraitor = GameMain.Client?.Character?.IsTraitor ?? false; - - var missionFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, isTraitor ? 0.95f : 0.45f), infoFrameContent.RectTransform)) - { - RelativeSpacing = 0.05f - }; - - if (Mission != null) - { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionFrame.RectTransform), Mission.Name, font: GUI.LargeFont); - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionFrame.RectTransform), TextManager.GetWithVariable("MissionReward", "[reward]", Mission.Reward.ToString())); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionFrame.RectTransform), Mission.Description, wrap: true); - } - else - { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionFrame.RectTransform, Anchor.TopCenter), TextManager.Get("NoMission"), font: GUI.LargeFont); - } - if (isTraitor) - { - var traitorFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.45f), infoFrameContent.RectTransform, Anchor.BottomLeft)) - { - RelativeSpacing = 0.05f - }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), traitorFrame.RectTransform), TextManager.Get("Traitors"), font: GUI.LargeFont); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), traitorFrame.RectTransform), GameMain.Client.Character.TraitorCurrentObjective, wrap: true); - } - } - public void AddToGUIUpdateList() { if (GUI.DisableHUD) return; GameMode?.AddToGUIUpdateList(); - infoFrame?.AddToGUIUpdateList(); + tabMenu?.AddToGUIUpdateList(); if (GameMain.NetworkMember != null) { @@ -166,17 +48,31 @@ namespace Barotrauma { if (GUI.DisableHUD) return; - if (PlayerInput.KeyDown(InputType.InfoTab) && - (GUI.KeyboardDispatcher.Subscriber == null || GUI.KeyboardDispatcher.Subscriber is GUIListBox)) + if (GameMode.IsRunning) { - if (infoFrame == null) + if (tabMenu == null) { - ToggleInfoFrame(); + if (PlayerInput.KeyHit(InputType.InfoTab) && GUI.KeyboardDispatcher.Subscriber is GUITextBox == false) + { + ToggleTabMenu(); + } + } + else + { + tabMenu.Update(); + + if (PlayerInput.KeyHit(InputType.InfoTab) && GUI.KeyboardDispatcher.Subscriber is GUITextBox == false) + { + ToggleTabMenu(); + } } } - else if (infoFrame != null) + else { - ToggleInfoFrame(); + if (tabMenu != null) + { + ToggleTabMenu(); + } } if (GameMain.NetworkMember != null) @@ -202,9 +98,7 @@ namespace Barotrauma public void Draw(SpriteBatch spriteBatch) { if (GUI.DisableHUD) return; - GameMode?.Draw(spriteBatch); - //infoFrame?.DrawManually(spriteBatch); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index 78b8f52fe..12cd4a55e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -32,7 +32,9 @@ namespace Barotrauma SoundPlayer.OverrideMusicDuration = 18.0f; } - GUIFrame frame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), style: "GUIBackgroundBlocker") + GUIFrame background = new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas, Anchor.Center), style: "GUIBackgroundBlocker"); + + GUIFrame frame = new GUIFrame(new RectTransform(Vector2.One, background.RectTransform, Anchor.Center), style: null) { UserData = "roundsummary" }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs index 0bea0a5b6..c3f9fb6a3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs @@ -18,6 +18,7 @@ namespace Barotrauma { Graphics, Audio, + VoiceChat, Controls, #if DEBUG Debug @@ -43,6 +44,8 @@ namespace Barotrauma } } + private const int inventoryHotkeyCount = 10; + public void SetDefaultBindings(XDocument doc = null, bool legacy = false) { keyMapping = new KeyOrMouse[Enum.GetNames(typeof(InputType)).Length]; @@ -60,6 +63,7 @@ namespace Barotrauma keyMapping[(int)InputType.CrewOrders] = new KeyOrMouse(Keys.C); keyMapping[(int)InputType.Voice] = new KeyOrMouse(Keys.V); + keyMapping[(int)InputType.LocalVoice] = new KeyOrMouse(Keys.B); keyMapping[(int)InputType.Command] = new KeyOrMouse(MouseButton.MiddleMouse); if (Language == "French") @@ -98,6 +102,13 @@ namespace Barotrauma keyMapping[(int)InputType.Select] = new KeyOrMouse(MouseButton.PrimaryMouse); // shoot and deselect are handled in CheckBindings() so that we don't override the legacy settings. } + + inventoryKeyMapping = new KeyOrMouse[inventoryHotkeyCount]; + for (int i = 0; i < inventoryKeyMapping.Length; i++) + { + inventoryKeyMapping[i] = new KeyOrMouse(Keys.D0 + (i + 1) % 10); + } + if (doc != null) { LoadControls(doc); @@ -177,6 +188,26 @@ namespace Barotrauma } } + private void LoadInventoryKeybinds(XElement element) + { + for (int i = 0; i < inventoryKeyMapping.Length; i++) + { + XAttribute attribute = element.Attributes().ElementAt(i); + if (int.TryParse(attribute.Value.ToString(), out int mouseButtonInt)) + { + inventoryKeyMapping[i] = new KeyOrMouse((MouseButton)mouseButtonInt); + } + else if (Enum.TryParse(attribute.Value.ToString(), true, out MouseButton mouseButton)) + { + inventoryKeyMapping[i] = new KeyOrMouse(mouseButton); + } + else if (Enum.TryParse(attribute.Value.ToString(), true, out Keys key)) + { + inventoryKeyMapping[i] = new KeyOrMouse(key); + } + } + } + private void LoadControls(XDocument doc) { XElement keyMapping = doc.Root.Element("keymapping"); @@ -184,6 +215,12 @@ namespace Barotrauma { LoadKeyBinds(keyMapping); } + + XElement inventoryKeyMapping = doc.Root.Element("inventorykeymapping"); + if (inventoryKeyMapping != null) + { + LoadInventoryKeybinds(inventoryKeyMapping); + } } public KeyOrMouse KeyBind(InputType inputType) @@ -193,25 +230,14 @@ namespace Barotrauma public string KeyBindText(InputType inputType) { - KeyOrMouse bind = keyMapping[(int)inputType]; - - if (bind.MouseButton != MouseButton.None) - { - switch (bind.MouseButton) - { - case MouseButton.PrimaryMouse: - return PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.rightmouse") : TextManager.Get("input.leftmouse"); - case MouseButton.SecondaryMouse: - return PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.leftmouse") : TextManager.Get("input.rightmouse"); - default: - return TextManager.Get("input." + bind.MouseButton.ToString().ToLowerInvariant()); - - } - } - - return bind.ToString(); + return keyMapping[(int)inputType].Name; } - + + public KeyOrMouse InventoryKeyBind(int index) + { + return inventoryKeyMapping[index]; + } + private GUIListBox contentPackageList; private bool ChangeSliderText(GUIScrollBar scrollBar, float barScroll) @@ -257,7 +283,8 @@ namespace Barotrauma } else { - settingsFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), style: null, color: Color.Black * 0.5f); + settingsFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null); + new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, settingsFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); var settingsFrameContent = new GUIFrame(new RectTransform(new Vector2(0.8f, 0.8f), settingsFrame.RectTransform, Anchor.Center)); settingsHolder = settingsFrameContent.RectTransform; } @@ -290,6 +317,25 @@ namespace Barotrauma ButtonEnabled = ContentPackage.List.Count(cp => cp.CorePackage) > 1 }; + var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), leftPanel.RectTransform), isHorizontal: true) + { + Stretch = true + }; + var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUI.Font); + var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform, Anchor.CenterRight), font: GUI.Font, createClearButton: true); + filterContainer.RectTransform.MinSize = searchBox.RectTransform.MinSize; + searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; + searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; + searchBox.OnTextChanged += (textBox, text) => + { + foreach (GUIComponent child in contentPackageList.Content.Children) + { + if (!(child.UserData is ContentPackage cp)) { continue; } + child.Visible = string.IsNullOrEmpty(text) ? true : cp.Name.ToLower().Contains(text.ToLower()); + } + return true; + }; + contentPackageList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.70f), leftPanel.RectTransform)) { OnSelected = (gc, obj) => false, @@ -443,11 +489,13 @@ namespace Barotrauma UserData = tab }; + float tabWidth = 0.25f; #if DEBUG + tabWidth = 0.2f; if (tab != Tab.Debug) { #endif - tabButtons[(int)tab] = new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), tabButtonHolder.RectTransform), + tabButtons[(int)tab] = new GUIButton(new RectTransform(new Vector2(tabWidth, 1.0f), tabButtonHolder.RectTransform), TextManager.Get("SettingsTab." + tab.ToString()), style: "GUITabButton") { UserData = tab, @@ -457,7 +505,7 @@ namespace Barotrauma } else { - tabButtons[(int)tab] = new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), tabButtonHolder.RectTransform), "Debug", style: "GUITabButton") + tabButtons[(int)tab] = new GUIButton(new RectTransform(new Vector2(tabWidth, 1.0f), tabButtonHolder.RectTransform), "Debug", style: "GUITabButton") { UserData = tab, OnClicked = (bt, userdata) => { SelectTab((Tab)userdata); return true; } @@ -547,6 +595,37 @@ namespace Barotrauma }; + GUITickBox textureCompressionTickBox = new GUITickBox(new RectTransform(tickBoxScale, leftColumn.RectTransform), TextManager.Get("EnableTextureCompression")) + { + ToolTip = TextManager.Get("EnableTextureCompressionToolTip"), + OnSelected = (GUITickBox box) => + { + if (box.Selected == TextureCompressionEnabled) { return true; } + bool prevTextureCompressionEnabled = TextureCompressionEnabled; + TextureCompressionEnabled = box.Selected; + + var msgBox = new GUIMessageBox( + TextManager.Get("RestartRequiredLabel"), + TextManager.Get("RestartRequiredGeneric"), + buttons: new string[] { TextManager.Get("OK"), TextManager.Get("Cancel") }); + msgBox.Buttons[0].OnClicked += (btn, userdata) => + { + ApplySettings(); + GameMain.Instance.Exit(); + return true; + }; msgBox.Buttons[1].OnClicked += (btn, userdata) => + { + TextureCompressionEnabled = prevTextureCompressionEnabled; + box.Selected = prevTextureCompressionEnabled; + msgBox.Close(); + return true; + }; + + return true; + }, + Selected = TextureCompressionEnabled + }; + GUITickBox pauseOnFocusLostBox = new GUITickBox(new RectTransform(tickBoxScale, leftColumn.RectTransform), TextManager.Get("PauseOnFocusLost")) { @@ -608,7 +687,8 @@ namespace Barotrauma { ChangeSliderText(scrollBar, barScroll); LightMapScale = MathHelper.Lerp(0.2f, 1.0f, barScroll); - UnsavedSettings = true; return true; + UnsavedSettings = true; + return true; }, Step = 0.25f }; @@ -677,10 +757,10 @@ namespace Barotrauma var audioContent = new GUILayoutGroup(new RectTransform(new Vector2(0.97f, 0.97f), tabs[(int)Tab.Audio].RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) { - Stretch = true, + Stretch = false, RelativeSpacing = 0.01f }; - + GUITextBlock soundVolumeText = new GUITextBlock(new RectTransform(textBlockScale, audioContent.RectTransform), TextManager.Get("SoundVolume"), font: GUI.SubHeadingFont); GUIScrollBar soundScrollBar = new GUIScrollBar(new RectTransform(textBlockScale, audioContent.RectTransform), style: "GUISlider", barSize: 0.05f) @@ -718,15 +798,16 @@ namespace Barotrauma style: "GUISlider", barSize: 0.05f) { UserData = voiceChatVolumeText, - BarScroll = VoiceChatVolume, - OnMoved = (scrollBar, scroll) => - { - ChangeSliderText(scrollBar, scroll); - VoiceChatVolume = scroll; - return true; - }, + Range = new Vector2(0.0f, 2.0f), Step = 0.05f }; + voiceChatScrollBar.BarScrollValue = VoiceChatVolume; + voiceChatScrollBar.OnMoved = (scrollBar, scroll) => + { + ChangeSliderText(scrollBar, scrollBar.BarScrollValue); + VoiceChatVolume = scrollBar.BarScrollValue; + return true; + }; voiceChatScrollBar.OnMoved(voiceChatScrollBar, voiceChatScrollBar.BarScroll); GUITickBox muteOnFocusLostBox = new GUITickBox(new RectTransform(tickBoxScale, audioContent.RectTransform), TextManager.Get("MuteOnFocusLost")) @@ -765,7 +846,15 @@ namespace Barotrauma } }; - new GUITextBlock(new RectTransform(textBlockScale, audioContent.RectTransform), TextManager.Get("VoiceChat"), font: GUI.SubHeadingFont); + /// Voice chat tab ---------------------------------------------------------------- + + var voiceChatContent = new GUILayoutGroup(new RectTransform(new Vector2(0.97f, 0.97f), tabs[(int)Tab.VoiceChat].RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) + { + Stretch = false, + RelativeSpacing = 0.01f + }; + + //new GUITextBlock(new RectTransform(textBlockScale, voiceChatContent.RectTransform), TextManager.Get("VoiceChat"), font: GUI.SubHeadingFont); CaptureDeviceNames = Alc.GetStringList((IntPtr)null, Alc.CaptureDeviceSpecifier); foreach (string name in CaptureDeviceNames) @@ -773,7 +862,7 @@ namespace Barotrauma DebugConsole.NewMessage(name + " " + name.Length.ToString(), Color.Lime); } - GUITickBox directionalVoiceChat = new GUITickBox(new RectTransform(tickBoxScale, audioContent.RectTransform), TextManager.Get("DirectionalVoiceChat")) + GUITickBox directionalVoiceChat = new GUITickBox(new RectTransform(tickBoxScale, voiceChatContent.RectTransform), TextManager.Get("DirectionalVoiceChat")) { Selected = UseDirectionalVoiceChat, ToolTip = TextManager.Get("DirectionalVoiceChatToolTip"), @@ -794,7 +883,7 @@ namespace Barotrauma VoiceSetting = VoiceMode.Disabled; } #if (!OSX) - var deviceList = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.15f), audioContent.RectTransform), TrimAudioDeviceName(VoiceCaptureDevice), CaptureDeviceNames.Count); + var deviceList = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.15f), voiceChatContent.RectTransform), TrimAudioDeviceName(VoiceCaptureDevice), CaptureDeviceNames.Count); if (CaptureDeviceNames?.Count > 0) { foreach (string name in CaptureDeviceNames) @@ -819,7 +908,7 @@ namespace Barotrauma } #else - var defaultDeviceGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.3f), audioContent.RectTransform), true, Anchor.CenterLeft); + var defaultDeviceGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.3f), voiceChatContent.RectTransform), true, Anchor.CenterLeft); var currentDeviceTextBlock = new GUITextBlock(new RectTransform(new Vector2(.7f, 0.75f), null), TextManager.AddPunctuation(':', TextManager.Get("CurrentDevice"), TrimAudioDeviceName(VoiceCaptureDevice)), font: GUI.SubHeadingFont) { @@ -857,15 +946,15 @@ namespace Barotrauma #endif var voiceModeCount = Enum.GetNames(typeof(VoiceMode)).Length; - var voiceModeDropDown = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.15f), audioContent.RectTransform), elementCount: voiceModeCount); + var voiceModeDropDown = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.15f), voiceChatContent.RectTransform), elementCount: voiceModeCount); for (int i = 0; i < voiceModeCount; i++) { var voiceMode = "VoiceMode." + ((VoiceMode)i).ToString(); voiceModeDropDown.AddItem(TextManager.Get(voiceMode), userData: i, toolTip: TextManager.Get(voiceMode + "ToolTip")); } - var micVolumeText = new GUITextBlock(new RectTransform(textBlockScale, audioContent.RectTransform), TextManager.Get("MicrophoneVolume"), font: GUI.SubHeadingFont); - var micVolumeSlider = new GUIScrollBar(new RectTransform(textBlockScale, audioContent.RectTransform), + var micVolumeText = new GUITextBlock(new RectTransform(textBlockScale, voiceChatContent.RectTransform), TextManager.Get("MicrophoneVolume"), font: GUI.SubHeadingFont); + var micVolumeSlider = new GUIScrollBar(new RectTransform(textBlockScale, voiceChatContent.RectTransform), style: "GUISlider", barSize: 0.05f) { UserData = micVolumeText, @@ -882,7 +971,7 @@ namespace Barotrauma }; micVolumeSlider.OnMoved(micVolumeSlider, micVolumeSlider.BarScroll); - var extraVoiceSettingsContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.25f), audioContent.RectTransform, Anchor.BottomCenter), style: null); + var extraVoiceSettingsContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), voiceChatContent.RectTransform, Anchor.BottomCenter), style: null); var voiceActivityGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), extraVoiceSettingsContainer.RectTransform)) { @@ -928,8 +1017,8 @@ namespace Barotrauma return true; }; - var voiceInputContainer = new GUILayoutGroup( - new RectTransform(new Vector2(1.0f, 0.25f), extraVoiceSettingsContainer.RectTransform) + var voiceInputContainerHorizontal = new GUILayoutGroup( + new RectTransform(new Vector2(1.0f, 0.5f), extraVoiceSettingsContainer.RectTransform) { RelativeOffset = new Vector2(0.0f, voiceActivityGroup.RectTransform.RelativeSize.Y + 0.1f) }, @@ -937,6 +1026,11 @@ namespace Barotrauma { Visible = VoiceSetting == VoiceMode.PushToTalk }; + + var voiceInputContainer = new GUILayoutGroup( + new RectTransform(new Vector2(0.5f, 1.0f), voiceInputContainerHorizontal.RectTransform), + isHorizontal: true, childAnchor: Anchor.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), voiceInputContainer.RectTransform), TextManager.Get("InputType.Voice"), font: GUI.SubHeadingFont); var voiceKeyBox = new GUITextBox(new RectTransform(new Vector2(0.4f, 1.0f), voiceInputContainer.RectTransform, Anchor.TopRight), text: KeyBindText(InputType.Voice)) { @@ -945,6 +1039,39 @@ namespace Barotrauma }; voiceKeyBox.OnSelected += KeyBoxSelected; + var localVoiceInputContainer = new GUILayoutGroup( + new RectTransform(new Vector2(0.5f, 1.0f), voiceInputContainerHorizontal.RectTransform), + isHorizontal: true, childAnchor: Anchor.CenterLeft); + + new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), localVoiceInputContainer.RectTransform), TextManager.Get("InputType.LocalVoice"), font: GUI.SubHeadingFont); + var localVoiceKeyBox = new GUITextBox(new RectTransform(new Vector2(0.4f, 1.0f), localVoiceInputContainer.RectTransform, Anchor.TopRight), text: KeyBindText(InputType.LocalVoice)) + { + SelectedColor = Color.Gold * 0.3f, + UserData = InputType.LocalVoice + }; + localVoiceKeyBox.OnSelected += KeyBoxSelected; + + var cutoffPreventionText = new GUITextBlock(new RectTransform(textBlockScale, voiceChatContent.RectTransform), TextManager.Get("CutoffPrevention"), font: GUI.SubHeadingFont) + { + ToolTip = TextManager.Get("CutoffPreventionTooltip") + }; + var cutoffPreventionSlider = new GUIScrollBar(new RectTransform(textBlockScale, voiceChatContent.RectTransform), + style: "GUISlider", barSize: 0.05f) + { + UserData = micVolumeText, + Range = new Vector2(0,540), + Step = 1.0f / 9.0f + }; + cutoffPreventionSlider.BarScrollValue = VoiceChatCutoffPrevention; + cutoffPreventionSlider.OnMoved = (scrollBar, scroll) => + { + VoiceChatCutoffPrevention = (int)scrollBar.BarScrollValue; + cutoffPreventionText.Text = TextManager.Get("CutoffPrevention") + + " " + TextManager.GetWithVariable("timeformatmilliseconds", "[milliseconds]", VoiceChatCutoffPrevention.ToString()); + return true; + }; + cutoffPreventionSlider.OnMoved(cutoffPreventionSlider, cutoffPreventionSlider.BarScrollValue); + voiceModeDropDown.OnSelected = (GUIComponent selected, object userData) => { try @@ -961,7 +1088,7 @@ namespace Barotrauma { VoiceSetting = vMode = VoiceMode.Disabled; voiceActivityGroup.Visible = false; - voiceInputContainer.Visible = false; + voiceInputContainerHorizontal.Visible = false; return true; } } @@ -977,7 +1104,7 @@ namespace Barotrauma noiseGateText.Visible = (vMode == VoiceMode.Activity); noiseGateSlider.Visible = (vMode == VoiceMode.Activity); voiceActivityGroup.Visible = (vMode != VoiceMode.Disabled); - voiceInputContainer.Visible = (vMode == VoiceMode.PushToTalk); + voiceInputContainerHorizontal.Visible = (vMode == VoiceMode.PushToTalk); UnsavedSettings = true; } catch (Exception e) @@ -1062,6 +1189,24 @@ namespace Barotrauma keyBox.SelectedColor = Color.Gold * 0.3f; } + for (int i = 0; i < inventoryHotkeyCount; i++) + { + var inputContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.06f), ((i + 1) <= inventoryHotkeyCount / 2 ? inputColumnLeft : inputColumnRight).RectTransform)) + { Stretch = true, IsHorizontal = true, RelativeSpacing = 0.01f, Color = new Color(12, 14, 15, 215) }; + var inputName = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), inputContainer.RectTransform, Anchor.TopLeft) { MinSize = new Point(100, 0) }, + TextManager.GetWithVariable("inventoryslotkeybind", "[slotnumber]", (i + 1).ToString()), font: GUI.SmallFont) + { ForceUpperCase = true }; + inputNameBlocks.Add(inputName); + var keyBox = new GUITextBox(new RectTransform(new Vector2(0.4f, 1.0f), inputContainer.RectTransform), + text: inventoryKeyMapping[i].Name, font: GUI.SmallFont, style: "GUITextBoxNoIcon") + { + UserData = i + }; + keyBox.Text = ToolBox.LimitString(keyBox.Text, keyBox.Font, (int)(keyBox.Rect.Width - keyBox.Padding.X - keyBox.Padding.Z)); + keyBox.OnSelected += InventoryKeyBoxSelected; + keyBox.SelectedColor = Color.Gold * 0.3f; + } + GUITextBlock.AutoScaleAndNormalize(inputNameBlocks); var resetControlsArea = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.07f), controlsLayoutGroup.RectTransform), style: null); @@ -1302,7 +1447,7 @@ namespace Barotrauma { switch (tab) { - case Tab.Audio: + case Tab.VoiceChat: if (VoiceSetting != VoiceMode.Disabled) { if (GameMain.Client == null && VoipCapture.Instance == null) @@ -1329,7 +1474,13 @@ namespace Barotrauma private void KeyBoxSelected(GUITextBox textBox, Keys key) { textBox.Text = ""; - CoroutineManager.StartCoroutine(WaitForKeyPress(textBox)); + CoroutineManager.StartCoroutine(WaitForKeyPress(textBox, keyMapping)); + } + + private void InventoryKeyBoxSelected(GUITextBox textBox, Keys key) + { + textBox.Text = ""; + CoroutineManager.StartCoroutine(WaitForKeyPress(textBox, inventoryKeyMapping)); } private void ResetControls(bool legacy) @@ -1425,7 +1576,7 @@ namespace Barotrauma return true; } - private IEnumerable WaitForKeyPress(GUITextBox keyBox) + private IEnumerable WaitForKeyPress(GUITextBox keyBox, KeyOrMouse[] keyArray) { yield return CoroutineStatus.Running; @@ -1449,42 +1600,43 @@ namespace Barotrauma if (PlayerInput.LeftButtonClicked()) { - keyMapping[keyIndex] = new KeyOrMouse(MouseButton.LeftMouse); + keyArray[keyIndex] = new KeyOrMouse(MouseButton.LeftMouse); } else if (PlayerInput.RightButtonClicked()) { - keyMapping[keyIndex] = new KeyOrMouse(MouseButton.RightMouse); + keyArray[keyIndex] = new KeyOrMouse(MouseButton.RightMouse); } else if (PlayerInput.MidButtonClicked()) { - keyMapping[keyIndex] = new KeyOrMouse(MouseButton.MiddleMouse); + keyArray[keyIndex] = new KeyOrMouse(MouseButton.MiddleMouse); } else if (PlayerInput.Mouse4ButtonClicked()) { - keyMapping[keyIndex] = new KeyOrMouse(MouseButton.MouseButton4); + keyArray[keyIndex] = new KeyOrMouse(MouseButton.MouseButton4); } else if (PlayerInput.Mouse5ButtonClicked()) { - keyMapping[keyIndex] = new KeyOrMouse(MouseButton.MouseButton5); + keyArray[keyIndex] = new KeyOrMouse(MouseButton.MouseButton5); } else if (PlayerInput.MouseWheelUpClicked()) { - keyMapping[keyIndex] = new KeyOrMouse(MouseButton.MouseWheelUp); + keyArray[keyIndex] = new KeyOrMouse(MouseButton.MouseWheelUp); } else if (PlayerInput.MouseWheelDownClicked()) { - keyMapping[keyIndex] = new KeyOrMouse(MouseButton.MouseWheelDown); + keyArray[keyIndex] = new KeyOrMouse(MouseButton.MouseWheelDown); } else if (PlayerInput.GetKeyboardState.GetPressedKeys().Length > 0) { Keys key = PlayerInput.GetKeyboardState.GetPressedKeys()[0]; - keyMapping[keyIndex] = new KeyOrMouse(key); + keyArray[keyIndex] = new KeyOrMouse(key); } else { yield return CoroutineStatus.Success; } - keyBox.Text = KeyBindText((InputType)keyIndex); + + keyBox.Text = keyArray[keyIndex].Name; keyBox.Text = ToolBox.LimitString(keyBox.Text, keyBox.Font, keyBox.Rect.Width); keyBox.Deselect(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 42de38bdd..85e6546d9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -345,9 +345,7 @@ namespace Barotrauma { hideButton.RectTransform.SetPosition(Anchor.TopLeft, Pivot.TopLeft); hideButton.RectTransform.NonScaledSize = new Point(HideButtonWidth, HUDLayoutSettings.BottomRightInfoArea.Height); - hideButton.RectTransform.AbsoluteOffset = new Point( - personalSlotArea.Right + Spacing * 2, - HUDLayoutSettings.BottomRightInfoArea.Y); + hideButton.RectTransform.AbsoluteOffset = new Point(HUDLayoutSettings.BottomRightInfoArea.Left - HideButtonWidth + GUI.IntScaleCeiling(2f), HUDLayoutSettings.BottomRightInfoArea.Y + GUI.IntScaleCeiling(1f)); hideButton.Visible = true; SetIndicatorSizes(); @@ -356,7 +354,6 @@ namespace Barotrauma break; case Layout.Right: { - int extraOffset = 0; int x = HUDLayoutSettings.InventoryAreaLower.Right; int personalSlotX = HUDLayoutSettings.InventoryAreaLower.Right - SlotSize.X - Spacing; for (int i = 0; i < slots.Length; i++) @@ -373,17 +370,18 @@ namespace Barotrauma } int lowerX = x; + int personalSlotY = GameMain.GraphicsHeight - bottomOffset * 2 - Spacing * 2 - (int)(!GUI.IsFourByThree() ? UnequippedIndicator.size.Y * UIScale * IndicatorScaleAdjustment : UnequippedIndicator.size.Y * UIScale * IndicatorScaleAdjustment * 2f); for (int i = 0; i < SlotPositions.Length; i++) { if (HideSlot(i)) continue; if (PersonalSlots.HasFlag(SlotTypes[i])) { - SlotPositions[i] = new Vector2(personalSlotX, GameMain.GraphicsHeight - bottomOffset * 2 - extraOffset - Spacing * 2); + SlotPositions[i] = new Vector2(personalSlotX, personalSlotY); personalSlotX -= slots[i].Rect.Width + Spacing; } else { - SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset - extraOffset); + SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset); x += slots[i].Rect.Width + Spacing; } } @@ -393,7 +391,7 @@ namespace Barotrauma { if (!HideSlot(i)) continue; x -= slots[i].Rect.Width + Spacing; - SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset - extraOffset); + SlotPositions[i] = new Vector2(x, GameMain.GraphicsHeight - bottomOffset); } } break; @@ -401,12 +399,14 @@ namespace Barotrauma { int x = HUDLayoutSettings.InventoryAreaLower.X; int personalSlotX = x; + int personalSlotY = GameMain.GraphicsHeight - bottomOffset * 2 - Spacing * 2 - (int)(!GUI.IsFourByThree() ? UnequippedIndicator.size.Y * UIScale * IndicatorScaleAdjustment : UnequippedIndicator.size.Y * UIScale * IndicatorScaleAdjustment * 2f); + for (int i = 0; i < SlotPositions.Length; i++) { if (HideSlot(i)) continue; if (PersonalSlots.HasFlag(SlotTypes[i])) { - SlotPositions[i] = new Vector2(personalSlotX, GameMain.GraphicsHeight - bottomOffset * 2 - Spacing * 2); + SlotPositions[i] = new Vector2(personalSlotX, personalSlotY); personalSlotX += slots[i].Rect.Width + Spacing; } else @@ -493,7 +493,7 @@ namespace Barotrauma ((selectedSlot != null && selectedSlot.IsSubSlot) || (draggingItem != null && (draggingSlot == null || !draggingSlot.MouseOn()))); if (CharacterHealth.OpenHealthWindow != null) hoverOnInventory = true; - if (layout == Layout.Default) + if (layout == Layout.Default && (Screen.Selected != GameMain.SubEditorScreen || Screen.Selected is SubEditorScreen editor && editor.WiringMode)) { if (hideButton.Visible) { @@ -525,8 +525,7 @@ namespace Barotrauma for (int i = 0; i < capacity; i++) { if (Items[i] != null && Items[i] != draggingItem && Character.Controlled?.Inventory == this && - GUI.KeyboardDispatcher.Subscriber == null && !CrewManager.IsCommandInterfaceOpen && - slots[i].QuickUseKey != Keys.None && PlayerInput.KeyHit(slots[i].QuickUseKey)) + GUI.KeyboardDispatcher.Subscriber == null && !CrewManager.IsCommandInterfaceOpen && PlayerInput.InventoryKeyHit(slots[i].InventoryKeyIndex)) { QuickUseItem(Items[i], true, false, true); } @@ -586,6 +585,19 @@ namespace Barotrauma } } } + + // In sub editor we cannot hover over the slot because they are not rendered so we override it here + if (Screen.Selected is SubEditorScreen subEditor && !subEditor.WiringMode) + { + for (int i = 0; i < slots.Length; i++) + { + var subInventory = GetSubInventory(i); + if (subInventory != null) + { + ShowSubInventory(new SlotReference(this, slots[i], i, false, Items[i].GetComponent().Inventory), deltaTime, cam, hideSubInventories, true); + } + } + } foreach (var subInventorySlot in hideSubInventories) { @@ -771,21 +783,16 @@ namespace Barotrauma } } - private void AssignQuickUseNumKeys() + public void AssignQuickUseNumKeys() { - int num = 1; + int keyBindIndex = 0; for (int i = 0; i < slots.Length; i++) { - if (HideSlot(i)) - { - slots[i].QuickUseKey = Keys.None; - continue; - } - + if (HideSlot(i)) continue; if (SlotTypes[i] == InvSlotType.Any) { - slots[i].QuickUseKey = Keys.D0 + num % 10; - num++; + slots[i].InventoryKeyIndex = keyBindIndex; + keyBindIndex++; } } } @@ -808,16 +815,21 @@ namespace Barotrauma { if (item.Container == null || character.Inventory.FindIndex(item.Container) == -1) // Not a subinventory in the character's inventory { - return item.ParentInventory is CharacterInventory ? - QuickUseAction.TakeFromCharacter : QuickUseAction.TakeFromContainer; + if (character.SelectedItems.Any(i => i?.OwnInventory != null && i.OwnInventory.CanBePut(item))) + { + return QuickUseAction.PutToEquippedItem; + } + else + { + return item.ParentInventory is CharacterInventory ? QuickUseAction.TakeFromCharacter : QuickUseAction.TakeFromContainer; + } } else { var selectedContainer = character.SelectedConstruction?.GetComponent(); if (selectedContainer != null && selectedContainer.Inventory != null && - !selectedContainer.Inventory.Locked && - allowInventorySwap) + !selectedContainer.Inventory.Locked) { // Move the item from the subinventory to the selected container return QuickUseAction.PutToContainer; @@ -854,7 +866,7 @@ namespace Barotrauma { return QuickUseAction.TakeFromCharacter; } - else if (character.SelectedItems.Any(i => i?.OwnInventory != null && i.OwnInventory.CanBePut(item))) + else if (character.SelectedItems.Any(i => i?.OwnInventory != null && i.OwnInventory.CanBePut(item)) && allowInventorySwap) { return QuickUseAction.PutToEquippedItem; } @@ -882,13 +894,40 @@ namespace Barotrauma private void QuickUseItem(Item item, bool allowEquip, bool allowInventorySwap, bool allowApplyTreatment) { + if (Screen.Selected is SubEditorScreen editor && !editor.WiringMode && !Submarine.Unloading) + { + // Find the slot the item was contained in and flash it + if (item.ParentInventory?.slots != null) + { + var invSlots = item.ParentInventory.slots; + var invItems = item.ParentInventory.Items; + for (int i = 0; i < invSlots.Length; i++) + { + if (i < 0 || invSlots.Length <= i || i < 0 || invItems.Length <= i) { break; } + + var slot = invSlots[i]; + var slotItem = invItems[i]; + + if (slotItem == item) + { + slot.ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.4f); + GUI.PlayUISound(GUISoundType.PickItem); + break; + } + } + } + + item.Remove(); + return; + } + var quickUseAction = GetQuickUseAction(item, allowEquip, allowInventorySwap, allowApplyTreatment); bool success = false; switch (quickUseAction) { case QuickUseAction.Equip: //attempt to put in a free slot first - for (int i = 0; i < capacity; i++) + for (int i = capacity - 1; i >= 0; i--) { if (Items[i] != null) continue; if (SlotTypes[i] == InvSlotType.Any || !item.AllowedSlots.Any(a => a.HasFlag(SlotTypes[i]))) continue; @@ -898,11 +937,11 @@ namespace Barotrauma if (!success) { - for (int i = 0; i < capacity; i++) + for (int i = capacity - 1; i >= 0; i--) { if (SlotTypes[i] == InvSlotType.Any || !item.AllowedSlots.Any(a => a.HasFlag(SlotTypes[i]))) continue; - //something else already equipped in the slot, attempt to unequip it - if (Items[i] != null && Items[i].AllowedSlots.Contains(InvSlotType.Any)) + // something else already equipped in a hand slot, attempt to unequip it so items aren't unnecessarily swapped to it + if (Items[i] != null && Items[i].AllowedSlots.Contains(InvSlotType.Any) && SlotTypes[i] == InvSlotType.LeftHand || SlotTypes[i] == InvSlotType.RightHand) { TryPutItem(Items[i], Character.Controlled, new List() { InvSlotType.Any }, true); } @@ -1003,6 +1042,7 @@ namespace Barotrauma prevUIScale != UIScale || prevHUDScale != GUI.Scale) { + CreateSlots(); SetSlotPositions(layout); prevUIScale = UIScale; prevHUDScale = GUI.Scale; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs index 9e751d1df..fb818670e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs @@ -237,6 +237,7 @@ namespace Barotrauma.Items.Components { StopPicking(null); PlaySound(forcedOpen ? ActionType.OnPicked : ActionType.OnUse); + if (isOpen) { stuck = MathHelper.Clamp(stuck - StuckReductionOnOpen, 0.0f, 100.0f); } } } } @@ -245,7 +246,8 @@ namespace Barotrauma.Items.Components { base.ClientRead(type, msg, sendingTime); - bool open = msg.ReadBoolean(); + bool open = msg.ReadBoolean(); + bool broken = msg.ReadBoolean(); bool forcedOpen = msg.ReadBoolean(); SetState(open, isNetworkMessage: true, sendNetworkMessage: false, forcedOpen: forcedOpen); Stuck = msg.ReadRangedSingle(0.0f, 100.0f, 8); @@ -258,6 +260,7 @@ namespace Barotrauma.Items.Components } if (isStuck) { OpenState = 0.0f; } + IsBroken = broken; PredictedState = null; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs index e2e03e335..b87d69d5e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; @@ -52,5 +53,13 @@ namespace Barotrauma.Items.Components } } } + + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + { + CurrPowerConsumption = powerConsumption; + charging = true; + timer = Duration; + IsActive = true; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs index 4d06b050d..f6abd8b28 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/Holdable.cs @@ -71,6 +71,8 @@ namespace Barotrauma.Items.Components base.ClientRead(type, msg, sendingTime); bool shouldBeAttached = msg.ReadBoolean(); Vector2 simPosition = new Vector2(msg.ReadSingle(), msg.ReadSingle()); + UInt16 submarineID = msg.ReadUInt16(); + Submarine sub = Entity.FindEntityByID(submarineID) as Submarine; if (!attachable) { @@ -84,6 +86,7 @@ namespace Barotrauma.Items.Components { Drop(false, null); item.SetTransform(simPosition, 0.0f); + item.Submarine = sub; AttachToWall(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs index 09c3d6896..3491978b7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Holdable/RangedWeapon.cs @@ -4,7 +4,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Text; using System.Xml.Linq; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index 9a76f7419..d1025621a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -4,7 +4,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; @@ -436,8 +436,7 @@ namespace Barotrauma.Items.Components if (subElement.Attribute("color") != null) color = subElement.GetAttributeColor("color", Color.White); string style = subElement.Attribute("style") == null ? null : subElement.GetAttributeString("style", ""); - - GuiFrame = new GUIFrame(RectTransform.Load(subElement, GUI.Canvas, Anchor.Center), style, color); + GuiFrame = new GUIFrame(RectTransform.Load(subElement, GUI.Canvas.ItemComponentHolder, Anchor.Center), style, color); DefaultLayout = GUILayoutSettings.Load(subElement); break; case "alternativelayout": diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs index aa81a968c..4bc8d1250 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemLabel.cs @@ -28,7 +28,7 @@ namespace Barotrauma.Items.Components } private string text; - [Serialize("", true, translationTextTag: "Label.", description: "The text displayed in the label."), Editable(100)] + [Serialize("", true, translationTextTag: "Label.", description: "The text displayed in the label.", alwaysUseInstanceValues: true), Editable(100)] public string Text { get { return text; } @@ -58,7 +58,7 @@ namespace Barotrauma.Items.Components private set; } - [Editable, Serialize("0,0,0,255", true, description: "The color of the text displayed on the label (R,G,B,A).")] + [Editable, Serialize("0,0,0,255", true, description: "The color of the text displayed on the label (R,G,B,A).", alwaysUseInstanceValues: true)] public Color TextColor { get { return textColor; } @@ -69,7 +69,7 @@ namespace Barotrauma.Items.Components } } - [Editable(0.0f, 10.0f), Serialize(1.0f, true, description: "The scale of the text displayed on the label.")] + [Editable(0.0f, 10.0f), Serialize(1.0f, true, description: "The scale of the text displayed on the label.", alwaysUseInstanceValues: true)] public float TextScale { get { return textBlock == null ? 1.0f : textBlock.TextScale; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index 51a3a14fd..823cdd6ea 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -48,7 +48,10 @@ namespace Barotrauma.Items.Components { if (light.LightSprite != null && (item.body == null || item.body.Enabled) && lightBrightness > 0.0f && IsOn) { - light.LightSprite.Draw(spriteBatch, new Vector2(item.DrawPosition.X, -item.DrawPosition.Y), lightColor * lightBrightness, 0.0f, item.Scale, SpriteEffects.None, item.SpriteDepth - 0.0001f); + Vector2 origin = light.LightSprite.Origin; + if (light.LightSpriteEffect == SpriteEffects.FlipHorizontally) { origin.X = light.LightSprite.SourceRect.Width - origin.X; } + if (light.LightSpriteEffect == SpriteEffects.FlipVertically) { origin.Y = light.LightSprite.SourceRect.Height - origin.Y; } + light.LightSprite.Draw(spriteBatch, new Vector2(item.DrawPosition.X, -item.DrawPosition.Y), lightColor * lightBrightness, origin, -light.Rotation, item.Scale, light.LightSpriteEffect, item.SpriteDepth - 0.0001f); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs index d19b33666..01439da52 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs @@ -76,7 +76,7 @@ namespace Barotrauma.Items.Components public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { - state = msg.ReadBoolean(); + State = msg.ReadBoolean(); ushort userID = msg.ReadUInt16(); if (userID == 0) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs index cadfb6011..6240e6ddf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Engine.cs @@ -16,6 +16,10 @@ namespace Barotrauma.Items.Components private GUIScrollBar forceSlider; private GUITickBox autoControlIndicator; + private int particlesPerSec = 60; + private float particleTimer; + + public float AnimSpeed { get; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index 89165ecc0..f08be819d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -318,10 +318,10 @@ namespace Barotrauma.Items.Components { if (item.ParentInventory.slots[availableSlotIndex].HighlightTimer <= 0.0f) { - item.ParentInventory.slots[availableSlotIndex].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f); + item.ParentInventory.slots[availableSlotIndex].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f, 0.2f); if (slotIndex < inputContainer.Capacity) { - inputContainer.Inventory.slots[slotIndex].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f); + inputContainer.Inventory.slots[slotIndex].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f, 0.2f); } } } @@ -337,10 +337,22 @@ namespace Barotrauma.Items.Components slotRect.Center.ToVector2(), color: requiredItem.ItemPrefab.InventoryIconColor * 0.3f, scale: Math.Min(slotRect.Width / itemIcon.size.X, slotRect.Height / itemIcon.size.Y)); - + + if (requiredItem.UseCondition && requiredItem.MinCondition < 1.0f) + { + GUI.DrawRectangle(spriteBatch, new Rectangle(slotRect.X, slotRect.Bottom - 8, slotRect.Width, 8), Color.Black * 0.8f, true); + GUI.DrawRectangle(spriteBatch, + new Rectangle(slotRect.X, slotRect.Bottom - 8, (int)(slotRect.Width * requiredItem.MinCondition), 8), + GUI.Style.Green * 0.8f, true); + } + if (slotRect.Contains(PlayerInput.MousePosition)) { string toolTipText = requiredItem.ItemPrefab.Name; + if (requiredItem.UseCondition && requiredItem.MinCondition < 1.0f) + { + toolTipText += " " + (int)Math.Round(requiredItem.MinCondition * 100) + "%"; + } if (!string.IsNullOrEmpty(requiredItem.ItemPrefab.Description)) { toolTipText += '\n' + requiredItem.ItemPrefab.Description; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index 6114dc576..b80dd19d0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -91,6 +91,7 @@ namespace Barotrauma.Items.Components if ((item.Submarine == null && displayedSubs.Count > 0) || //item not inside a sub anymore, but display is still showing subs !displayedSubs.Contains(item.Submarine) || //current sub not displayer item.Submarine.DockedTo.Any(s => !displayedSubs.Contains(s)) || //some of the docked subs not diplayed + !submarineContainer.Children.Any() || // We lack a GUI displayedSubs.Any(s => s != item.Submarine && !item.Submarine.DockedTo.Contains(s))) //displaying a sub that shouldn't be displayed { CreateHUD(); @@ -116,15 +117,17 @@ namespace Barotrauma.Items.Components private void DrawHUDFront(SpriteBatch spriteBatch, GUICustomComponent container) { if (Voltage < MinVoltage) - { + { Vector2 textSize = GUI.Font.MeasureString(noPowerTip); Vector2 textPos = GuiFrame.Rect.Center.ToVector2(); GUI.DrawString(spriteBatch, textPos - textSize / 2, noPowerTip, - GUI.Style.Orange * (float)Math.Abs(Math.Sin(Timing.TotalTime)), Color.Black * 0.8f, font: GUI.SubHeadingFont); + GUI.Style.Orange * (float)Math.Abs(Math.Sin(Timing.TotalTime)), Color.Black * 0.8f, font: GUI.SubHeadingFont); return; } + if (!submarineContainer.Children.Any()) { return; } + foreach (GUIComponent child in submarineContainer.Children.First().Children) { if (child.UserData is Hull hull) @@ -151,7 +154,7 @@ namespace Barotrauma.Items.Components foreach (Hull hull in Hull.hullList) { - var hullFrame = submarineContainer.Children.First().FindChild(hull); + var hullFrame = submarineContainer.Children.FirstOrDefault()?.FindChild(hull); if (hullFrame == null) { continue; } if (GUI.MouseOn == hullFrame || hullFrame.IsParentOf(GUI.MouseOn)) @@ -175,7 +178,7 @@ namespace Barotrauma.Items.Components foreach (Hull hull in Hull.hullList) { if (hull.Submarine == null) continue; - var hullFrame = submarineContainer.Children.First().FindChild(hull); + var hullFrame = submarineContainer.Children.FirstOrDefault()?.FindChild(hull); if (hullFrame == null) continue; hullDatas.TryGetValue(hull, out HullData hullData); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs index 069231f64..2632cc2cf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs @@ -163,7 +163,7 @@ namespace Barotrauma.Items.Components { pumpSpeedLockTimer -= deltaTime; isActiveLockTimer -= deltaTime; - autoControlIndicator.Selected = pumpSpeedLockTimer > 0.0f || isActiveLockTimer > 0.0f; + autoControlIndicator.Selected = IsAutoControlled; PowerButton.Enabled = isActiveLockTimer <= 0.0f; if (HasPower) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs index 1604387a7..2ddb5a88f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs @@ -192,9 +192,10 @@ namespace Barotrauma.Items.Components { RelativeOffset = new Vector2(0, fissionMeter.RectTransform.RelativeOffset.Y + meterSize.Y) }, - style: "DeviceSlider", barSize: 0.1f) + style: "DeviceSlider", barSize: 0.15f) { Enabled = false, + Step = 1.0f / 255, OnMoved = (GUIScrollBar bar, float scrollAmount) => { LastUser = Character.Controlled; @@ -209,9 +210,10 @@ namespace Barotrauma.Items.Components { RelativeOffset = new Vector2(0, turbineMeter.RectTransform.RelativeOffset.Y + meterSize.Y) }, - style: "DeviceSlider", barSize: 0.1f, isHorizontal: true) + style: "DeviceSlider", barSize: 0.15f, isHorizontal: true) { Enabled = false, + Step = 1.0f / 255, OnMoved = (GUIScrollBar bar, float scrollAmount) => { LastUser = Character.Controlled; @@ -715,8 +717,14 @@ namespace Barotrauma.Items.Components targetTurbineOutput = msg.ReadRangedSingle(0.0f, 100.0f, 8); degreeOfSuccess = msg.ReadRangedSingle(0.0f, 1.0f, 8); - FissionRateScrollBar.BarScroll = targetFissionRate / 100.0f; - TurbineOutputScrollBar.BarScroll = targetTurbineOutput / 100.0f; + if (Math.Abs(FissionRateScrollBar.BarScroll - targetFissionRate / 100.0f) > 0.01f) + { + FissionRateScrollBar.BarScroll = targetFissionRate / 100.0f; + } + if (Math.Abs(TurbineOutputScrollBar.BarScroll - targetTurbineOutput / 100.0f) > 0.01f) + { + TurbineOutputScrollBar.BarScroll = targetTurbineOutput / 100.0f; + } IsActive = true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index 2282053b9..b7df977f6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -18,6 +18,8 @@ namespace Barotrauma.Items.Components Disruption } + private PathFinder pathFinder; + private bool dynamicDockingIndicator = true; private bool unsentChanges; @@ -45,7 +47,7 @@ namespace Barotrauma.Items.Components private Sprite sonarBlip; private Sprite lineSprite; - private Dictionary targetIcons = new Dictionary(); + private readonly Dictionary targetIcons = new Dictionary(); private float displayBorderSize; @@ -65,7 +67,23 @@ namespace Barotrauma.Items.Components //Vector2 = vector from the ping source to the position of the disruption //float = strength of the disruption, between 0-1 - List> disruptedDirections = new List>(); + private readonly List> disruptedDirections = new List>(); + + class CachedDistance + { + public readonly Vector2 TransducerWorldPos; + public readonly Vector2 WorldPos; + public readonly float Distance; + + public CachedDistance(Vector2 transducerWorldPos, Vector2 worldPos, float dist) + { + TransducerWorldPos = transducerWorldPos; + WorldPos = worldPos; + Distance = dist; + } + } + + private readonly Dictionary markerDistances = new Dictionary(); private readonly Color positiveColor = Color.Green; private readonly Color warningColor = Color.Orange; @@ -74,7 +92,7 @@ namespace Barotrauma.Items.Components public static readonly Vector2 controlBoxSize = new Vector2(0.33f, 0.32f); public static readonly Vector2 controlBoxOffset = new Vector2(0.025f, 0); - public static readonly float sonarAreaSize = 1.09f; + private static readonly float sonarAreaSize = 1.09f; private static readonly Dictionary blipColorGradient = new Dictionary() { @@ -94,6 +112,8 @@ namespace Barotrauma.Items.Components public float DisplayRadius { get; private set; } + public static Vector2 GUISizeCalculation => Vector2.One * Math.Min(GUI.RelativeHorizontalAspectRatio, 1f) * sonarAreaSize; + partial void InitProjSpecific(XElement element) { System.Diagnostics.Debug.Assert(Enum.GetValues(typeof(BlipType)).Cast().All(t => blipColorGradient.ContainsKey(t))); @@ -254,7 +274,7 @@ namespace Barotrauma.Items.Components controlContainer.RectTransform.SetPosition(Anchor.TopLeft); sonarView.RectTransform.ScaleBasis = ScaleBasis.Smallest; sonarView.RectTransform.SetPosition(Anchor.CenterRight); - sonarView.RectTransform.Resize(Vector2.One * GUI.RelativeHorizontalAspectRatio * sonarAreaSize); + sonarView.RectTransform.Resize(GUISizeCalculation); GUITextBlock.AutoScaleAndNormalize(passiveTickBox.TextBlock, activeTickBox.TextBlock, zoomText, directionalModeSwitchText); } } @@ -490,6 +510,7 @@ namespace Barotrauma.Items.Components disruptedDirections.Clear(); foreach (AITarget t in AITarget.List) { + if (t.Entity is Character c && c.Params.HideInSonar) { continue; } if (t.SoundRange <= 0.0f || float.IsNaN(t.SoundRange) || float.IsInfinity(t.SoundRange)) { continue; } float distSqr = Vector2.DistanceSquared(t.WorldPosition, transducerCenter); @@ -652,12 +673,16 @@ namespace Barotrauma.Items.Components DrawMarker(spriteBatch, GameMain.GameSession.StartLocation.Name, "outpost", - (Level.Loaded.StartPosition - transducerCenter), displayScale, center, DisplayRadius); + GameMain.GameSession.StartLocation.Name, + Level.Loaded.StartPosition, transducerCenter, + displayScale, center, DisplayRadius); DrawMarker(spriteBatch, GameMain.GameSession.EndLocation.Name, "outpost", - (Level.Loaded.EndPosition - transducerCenter), displayScale, center, DisplayRadius); + GameMain.GameSession.EndLocation.Name, + Level.Loaded.EndPosition, transducerCenter, + displayScale, center, DisplayRadius); foreach (AITarget aiTarget in AITarget.List) { @@ -669,7 +694,9 @@ namespace Barotrauma.Items.Components DrawMarker(spriteBatch, aiTarget.SonarLabel, aiTarget.SonarIconIdentifier, - aiTarget.WorldPosition - transducerCenter, displayScale, center, DisplayRadius * 0.975f); + aiTarget, + aiTarget.WorldPosition, transducerCenter, + displayScale, center, DisplayRadius * 0.975f); } } @@ -684,7 +711,9 @@ namespace Barotrauma.Items.Components DrawMarker(spriteBatch, mission.SonarLabel, mission.SonarIconIdentifier, - sonarPosition - transducerCenter, displayScale, center, DisplayRadius * 0.95f); + mission, + sonarPosition, transducerCenter, + displayScale, center, DisplayRadius * 0.95f); } } } @@ -701,9 +730,10 @@ namespace Barotrauma.Items.Components if (sub.WorldPosition.Y > Level.Loaded.Size.Y) { continue; } DrawMarker(spriteBatch, - sub.Info.Name, + sub.Info.DisplayName, sub.Info.HasTag(SubmarineTag.Shuttle) ? "shuttle" : "submarine", - sub.WorldPosition - transducerCenter, + sub, + sub.WorldPosition, transducerCenter, displayScale, center, DisplayRadius * 0.95f); } @@ -801,8 +831,11 @@ namespace Barotrauma.Items.Components { if (Level.Loaded != null && dockingPort.Item.Submarine.WorldPosition.Y > Level.Loaded.Size.Y) { continue; } + if (dockingPort.Item.Submarine == null) { continue; } + if (dockingPort.Item.Submarine.Info.IsWreck) { continue; } + //don't show the docking ports of the opposing team on the sonar - if (item.Submarine != null && dockingPort.Item.Submarine != null) + if (item.Submarine != null) { if ((dockingPort.Item.Submarine.TeamID == Character.TeamType.Team1 && item.Submarine.TeamID == Character.TeamType.Team2) || (dockingPort.Item.Submarine.TeamID == Character.TeamType.Team2 && item.Submarine.TeamID == Character.TeamType.Team1)) @@ -947,14 +980,13 @@ namespace Barotrauma.Items.Components } foreach (AITarget aiTarget in AITarget.List) { - if (aiTarget.SonarDisruption <= 0.0f || !aiTarget.Enabled) { continue; } + float disruption = aiTarget.Entity is Character c ? c.Params.SonarDisruption : aiTarget.SonarDisruption; + if (disruption <= 0.0f || !aiTarget.Enabled) { continue; } float distSqr = Vector2.DistanceSquared(aiTarget.WorldPosition, pingSource); if (distSqr > worldPingRadiusSqr) { continue; } - float disruptionDist = (float)Math.Sqrt(distSqr); disruptedDirections.Add(new Pair((aiTarget.WorldPosition - pingSource) / disruptionDist, aiTarget.SonarDisruption)); - - CreateBlipsForDisruption(aiTarget.WorldPosition, aiTarget.SonarDisruption); + CreateBlipsForDisruption(aiTarget.WorldPosition, disruption); } } @@ -1127,6 +1159,7 @@ namespace Barotrauma.Items.Components foreach (Character c in Character.CharacterList) { if (c.AnimController.CurrentHull != null || !c.Enabled) { continue; } + if (c.Params.HideInSonar) { continue; } if (DetectSubmarineWalls && c.AnimController.CurrentHull == null && item.CurrentHull != null) { continue; } if (c.AnimController.SimplePhysicsEnabled) @@ -1298,9 +1331,42 @@ namespace Barotrauma.Items.Components sonarBlip.Draw(spriteBatch, center + pos, color * 0.5f, sonarBlip.Origin, 0, scale * 0.08f, SpriteEffects.None, 0); } - private void DrawMarker(SpriteBatch spriteBatch, string label, string iconIdentifier, Vector2 position, float scale, Vector2 center, float radius) + private void DrawMarker(SpriteBatch spriteBatch, string label, string iconIdentifier, object targetIdentifier, Vector2 worldPosition, Vector2 transducerPosition, float scale, Vector2 center, float radius) { - float dist = position.Length(); + float dist = Vector2.Distance(worldPosition, transducerPosition); + if (Vector2.DistanceSquared(worldPosition, transducerPosition) > Range * Range) + { + if (markerDistances.TryGetValue(targetIdentifier, out CachedDistance cachedDistance)) + { + if (Vector2.DistanceSquared(cachedDistance.TransducerWorldPos, transducerPosition) > 500 * 500 || + Vector2.DistanceSquared(cachedDistance.WorldPos, worldPosition) > 500 * 500) + { + markerDistances.Remove(targetIdentifier); + CalculateDistance(); + } + else + { + dist = cachedDistance.Distance; + } + } + else + { + CalculateDistance(); + } + } + + void CalculateDistance() + { + pathFinder ??= new PathFinder(WayPoint.WayPointList, indoorsSteering: false); + var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(transducerPosition), ConvertUnits.ToSimUnits(worldPosition)); + if (!path.Unreachable) + { + markerDistances.Add(targetIdentifier, new CachedDistance(transducerPosition, worldPosition, path.TotalLength)); + dist = path.TotalLength; + } + } + + Vector2 position = worldPosition - transducerPosition; position *= zoom; position *= scale; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index 6a51dc250..bbba34b4d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -390,7 +390,7 @@ namespace Barotrauma.Items.Components }; // Sonar area - steerArea = new GUICustomComponent(new RectTransform(Vector2.One * GUI.RelativeHorizontalAspectRatio * Sonar.sonarAreaSize, GuiFrame.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.Smallest), + steerArea = new GUICustomComponent(new RectTransform(Sonar.GUISizeCalculation, GuiFrame.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.Smallest), (spriteBatch, guiCustomComponent) => { DrawHUD(spriteBatch, guiCustomComponent.Rect); }, null); steerRadius = steerArea.Rect.Width / 2; @@ -667,7 +667,7 @@ namespace Barotrauma.Items.Components if (Vector2.DistanceSquared(PlayerInput.MousePosition, steerArea.Rect.Center.ToVector2()) < steerRadius * steerRadius) { - if (PlayerInput.PrimaryMouseButtonHeld() && !CrewManager.IsCommandInterfaceOpen && !GameSession.IsInfoFrameOpen) + if (PlayerInput.PrimaryMouseButtonHeld() && !CrewManager.IsCommandInterfaceOpen && !GameSession.IsTabMenuOpen) { Vector2 displaySubPos = (-sonar.DisplayOffset * sonar.Zoom) / sonar.Range * sonar.DisplayRadius * sonar.Zoom; displaySubPos.Y = -displaySubPos.Y; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs index 58e1a8209..765a44282 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs @@ -13,6 +13,8 @@ namespace Barotrauma.Items.Components bool isStuck = msg.ReadBoolean(); if (isStuck) { + ushort submarineID = msg.ReadUInt16(); + ushort hullID = msg.ReadUInt16(); Vector2 simPosition = new Vector2( msg.ReadSingle(), msg.ReadSingle()); @@ -20,7 +22,12 @@ namespace Barotrauma.Items.Components msg.ReadSingle(), msg.ReadSingle()); UInt16 entityID = msg.ReadUInt16(); - Entity entity = Entity.FindEntityByID(entityID); + + Entity entity = Entity.FindEntityByID(entityID); + Submarine submarine = Entity.FindEntityByID(submarineID) as Submarine; + Hull hull = Entity.FindEntityByID(hullID) as Hull; + item.Submarine = submarine; + item.CurrentHull = hull; item.body.SetTransform(simPosition, item.body.Rotation); if (entity is Character character) { @@ -30,12 +37,14 @@ namespace Barotrauma.Items.Components DebugConsole.ThrowError($"Failed to read a projectile update from the server. Limb index out of bounds ({limbIndex}, character: {character.ToString()})"); return; } + if (character.Removed) { return; } var limb = character.AnimController.Limbs[limbIndex]; StickToTarget(limb.body.FarseerBody, axis); } else if (entity is Structure structure) { byte bodyIndex = msg.ReadByte(); + if (bodyIndex == 255) { bodyIndex = 0; } if (bodyIndex >= structure.Bodies.Count) { DebugConsole.ThrowError($"Failed to read a projectile update from the server. Structure body index out of bounds ({bodyIndex}, structure: {structure.ToString()})"); @@ -46,6 +55,7 @@ namespace Barotrauma.Items.Components } else if (entity is Item item) { + if (item.Removed) { return; } StickToTarget(item.body.FarseerBody, axis); } else if (entity is Submarine sub) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index 998db3672..1391bcecd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -27,6 +27,8 @@ namespace Barotrauma.Items.Components private FixActions requestStartFixAction; + public float FakeBrokenTimer; + [Serialize("", false, description: "An optional description of the needed repairs displayed in the repair interface.")] public string Description { @@ -117,9 +119,18 @@ namespace Barotrauma.Items.Components case "emitter": case "particleemitter": particleEmitters.Add(new ParticleEmitter(subElement)); - particleEmitterConditionRanges.Add(new Vector2( - subElement.GetAttributeFloat("mincondition", 0.0f), - subElement.GetAttributeFloat("maxcondition", 100.0f))); + float minCondition = subElement.GetAttributeFloat("mincondition", 0.0f); + float maxCondition = subElement.GetAttributeFloat("maxcondition", 100.0f); + + if (maxCondition < minCondition) + { + DebugConsole.ThrowError("Invalid damage particle configuration in the Repairable component of " + item.Name + ". MaxCondition needs to be larger than MinCondition."); + float temp = maxCondition; + maxCondition = minCondition; + minCondition = temp; + } + particleEmitterConditionRanges.Add(new Vector2(minCondition, maxCondition)); + break; } } @@ -127,6 +138,9 @@ namespace Barotrauma.Items.Components partial void UpdateProjSpecific(float deltaTime) { + FakeBrokenTimer -= deltaTime; + item.FakeBroken = FakeBrokenTimer > 0.0f; + if (!GameMain.IsMultiplayer) { switch (requestStartFixAction) @@ -141,10 +155,10 @@ namespace Barotrauma.Items.Components break; } } - + for (int i = 0; i < particleEmitters.Count; i++) { - if (item.ConditionPercentage >= particleEmitterConditionRanges[i].X && item.ConditionPercentage <= particleEmitterConditionRanges[i].Y) + if ((item.ConditionPercentage >= particleEmitterConditionRanges[i].X && item.ConditionPercentage <= particleEmitterConditionRanges[i].Y) || FakeBrokenTimer > 0.0f) { particleEmitters[i].Emit(deltaTime, item.WorldPosition, item.CurrentHull); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs index 8bd5decf7..f72793dd5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs @@ -66,6 +66,15 @@ namespace Barotrauma.Items.Components if (target == null) { return; } Vector2 startPos = new Vector2(source.DrawPosition.X, -source.DrawPosition.Y); + var turret = source?.GetComponent(); + if (turret != null) + { + startPos = new Vector2(source.WorldRect.X + turret.TransformedBarrelPos.X, -(source.WorldRect.Y - turret.TransformedBarrelPos.Y)); + if (turret.BarrelSprite != null) + { + startPos += new Vector2((float)Math.Cos(turret.Rotation), (float)Math.Sin(turret.Rotation)) * turret.BarrelSprite.size.Y * turret.BarrelSprite.RelativeOrigin.Y * item.Scale * 0.9f; + } + } Vector2 endPos = new Vector2(target.DrawPosition.X, -target.DrawPosition.Y); if (Snapped) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs index 01cbec3df..4184e554e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Connection.cs @@ -379,7 +379,7 @@ namespace Barotrauma.Items.Components ConnectionPanel.HighlightedWire = wire; bool allowRewiring = GameMain.NetworkMember?.ServerSettings == null || GameMain.NetworkMember.ServerSettings.AllowRewiring; - if (allowRewiring && !wire.Locked && (!panel.Locked || Screen.Selected == GameMain.SubEditorScreen)) + if (allowRewiring && (!wire.Locked && !panel.Locked || Screen.Selected == GameMain.SubEditorScreen)) { //start dragging the wire if (PlayerInput.PrimaryMouseButtonHeld()) { DraggingConnected = wire; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs index 16e806bb2..0cfef7f33 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs @@ -56,15 +56,6 @@ namespace Barotrauma.Items.Components }; } - public override void OnItemLoaded() - { - base.OnItemLoaded(); - if (!string.IsNullOrEmpty(DisplayedWelcomeMessage)) - { - ShowOnDisplay(DisplayedWelcomeMessage); - } - } - private void SendOutput(string input) { if (input.Length > MaxMessageLength) @@ -123,6 +114,11 @@ namespace Barotrauma.Items.Components public override void AddToGUIUpdateList() { base.AddToGUIUpdateList(); + if (!string.IsNullOrEmpty(DisplayedWelcomeMessage)) + { + ShowOnDisplay(DisplayedWelcomeMessage); + DisplayedWelcomeMessage = ""; + } if (shouldSelectInputBox) { inputBox.Select(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs index bf7dd68a1..fc67bfeac 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs @@ -17,14 +17,60 @@ namespace Barotrauma.Items.Components partial class WireSection { + public VertexPositionColorTexture[] vertices; + public VertexPositionColorTexture[] shiftedVertices; + + private float cachedWidth = 0f; + + private void RecalculateVertices(Wire wire, float width) + { + if (MathUtils.NearlyEqual(cachedWidth, width)) { return; } + cachedWidth = width; + + vertices = new VertexPositionColorTexture[4]; + + Vector2 expandDir = start-end; + expandDir.Normalize(); + float temp = expandDir.X; + expandDir.X = -expandDir.Y; + expandDir.Y = -temp; + + Rectangle srcRect = wire.wireSprite.SourceRect; + + expandDir *= width * srcRect.Height * 0.5f; + + Vector2 rectLocation = srcRect.Location.ToVector2(); + Vector2 rectSize = srcRect.Size.ToVector2(); + Vector2 textureSize = new Vector2(wire.wireSprite.Texture.Width, wire.wireSprite.Texture.Height); + + Vector2 topLeftUv = rectLocation / textureSize; + Vector2 bottomRightUv = (rectLocation + rectSize) / textureSize; + + Vector2 invStart = new Vector2(start.X, -start.Y); + Vector2 invEnd = new Vector2(end.X, -end.Y); + + vertices[0] = new VertexPositionColorTexture(new Vector3(invStart + expandDir, 0f), Color.White, topLeftUv); + vertices[2] = new VertexPositionColorTexture(new Vector3(invEnd + expandDir, 0f), Color.White, new Vector2(bottomRightUv.X, topLeftUv.Y)); + vertices[1] = new VertexPositionColorTexture(new Vector3(invStart - expandDir, 0f), Color.White, new Vector2(topLeftUv.X, bottomRightUv.Y)); + vertices[3] = new VertexPositionColorTexture(new Vector3(invEnd - expandDir, 0f), Color.White, bottomRightUv); + + shiftedVertices = (VertexPositionColorTexture[])vertices.Clone(); + } + public void Draw(SpriteBatch spriteBatch, Wire wire, Color color, Vector2 offset, float depth, float width = 0.3f) { + if (width <= 0f) { return; } + RecalculateVertices(wire, width); + + for (int i=0;i wires) { + var doubleClicked = PlayerInput.DoubleClicked(); + Wire equippedWire = Character.Controlled?.SelectedItems[0]?.GetComponent() ?? Character.Controlled?.SelectedItems[1]?.GetComponent(); - if (equippedWire != null) + if (equippedWire != null && GUI.MouseOn == null) { if (PlayerInput.PrimaryMouseButtonClicked() && Character.Controlled.SelectedConstruction == null) { @@ -252,7 +300,7 @@ namespace Barotrauma.Items.Components } //dragging a node of some wire - if (draggingWire != null) + if (draggingWire != null && !doubleClicked) { if (Character.Controlled != null) { @@ -283,15 +331,18 @@ namespace Barotrauma.Items.Components if (selectedNodeIndex.HasValue) { - nodeWorldPos.X = MathUtils.Round(nodeWorldPos.X, Submarine.GridSize.X / 2.0f); - nodeWorldPos.Y = MathUtils.Round(nodeWorldPos.Y, Submarine.GridSize.Y / 2.0f); + if (!PlayerInput.IsShiftDown()) + { + nodeWorldPos.X = MathUtils.Round(nodeWorldPos.X, Submarine.GridSize.X / 2.0f); + nodeWorldPos.Y = MathUtils.Round(nodeWorldPos.Y, Submarine.GridSize.Y / 2.0f); + } draggingWire.nodes[(int)selectedNodeIndex] = nodeWorldPos; draggingWire.UpdateSections(); } else { - if (Vector2.DistanceSquared(nodeWorldPos, draggingWire.nodes[(int)highlightedNodeIndex]) > Submarine.GridSize.X * Submarine.GridSize.X) + if (Vector2.DistanceSquared(nodeWorldPos, draggingWire.nodes[(int)highlightedNodeIndex]) > Submarine.GridSize.X * Submarine.GridSize.X || PlayerInput.IsShiftDown()) { selectedNodeIndex = highlightedNodeIndex; } @@ -304,6 +355,8 @@ namespace Barotrauma.Items.Components return; } + bool updateHighlight = true; + //a wire has been selected -> check if we should start dragging one of the nodes float nodeSelectDist = 10, sectionSelectDist = 5; highlightedNodeIndex = null; @@ -359,6 +412,37 @@ namespace Barotrauma.Items.Components { selectedWire.nodes.RemoveAt(closestIndex); selectedWire.UpdateSections(); + } + // if only one end of the wire is disconnect pick it back up with double click + else if (doubleClicked && equippedWire == null && Character.Controlled != null && selectedWire.connections.Any(conn => conn != null)) + { + if (selectedWire.connections[0] == null && closestIndex == 0 || selectedWire.connections[1] == null && closestIndex == selectedWire.nodes.Count - 1) + { + selectedWire.IsActive = true; + selectedWire.nodes.RemoveAt(closestIndex); + selectedWire.UpdateSections(); + + // flip the wire + if (closestIndex == 0) + { + selectedWire.nodes.Reverse(); + selectedWire.connections[0] = selectedWire.connections[1]; + selectedWire.connections[1] = null; + } + + selectedWire.shouldClearConnections = false; + Character.Controlled.Inventory.TryPutItem(selectedWire.item, Character.Controlled, new List { InvSlotType.LeftHand, InvSlotType.RightHand }); + foreach (var entity in MapEntity.mapEntityList) + { + if (entity is Item item) + { + item.GetComponent()?.DisconnectedWires.Remove(selectedWire); + } + } + MapEntity.SelectedList.Clear(); + selectedWire.shouldClearConnections = true; + updateHighlight = false; + } } } } @@ -400,7 +484,7 @@ namespace Barotrauma.Items.Components } } - if (highlighted != null) + if (highlighted != null && updateHighlight) { highlighted.item.IsHighlighted = true; if (PlayerInput.PrimaryMouseButtonClicked()) @@ -411,6 +495,20 @@ namespace Barotrauma.Items.Components } } + public bool IsMouseOn() + { + if (GUI.MouseOn == null) + { + Vector2 mousePos = GameMain.SubEditorScreen.Cam.ScreenToWorld(PlayerInput.MousePosition); + if (item.Submarine != null) { mousePos -= (item.Submarine.Position + item.Submarine.HiddenSubPosition); } + + if (GetClosestNodeIndex(mousePos, 10, out _) > -1) { return true; } + if (GetClosestSectionIndex(mousePos, 10, out _) > -1) { return true; } + } + + return false; + } + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { int eventIndex = msg.ReadRangedInteger(0, (int)Math.Ceiling(MaxNodeCount / (float)MaxNodesPerNetworkEvent)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index 4b35cf0e6..bbf51b8e4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -5,7 +5,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; @@ -19,6 +19,8 @@ namespace Barotrauma.Items.Components private float recoilTimer; + private float RetractionTime => Math.Max(Reload * RetractionDurationMultiplier, RecoilTime); + private RoundSound startMoveSound, endMoveSound, moveSound; private SoundChannel moveSoundChannel; @@ -83,6 +85,11 @@ namespace Barotrauma.Items.Components } } + public Sprite BarrelSprite + { + get { return barrelSprite; } + } + partial void InitProjSpecific(XElement element) { foreach (XElement subElement in element.Elements()) @@ -126,7 +133,7 @@ namespace Barotrauma.Items.Components partial void LaunchProjSpecific() { - recoilTimer = Math.Max(Reload, 0.1f); + recoilTimer = RetractionTime; PlaySound(ActionType.OnUse); Vector2 particlePos = new Vector2(item.WorldRect.X + transformedBarrelPos.X, item.WorldRect.Y - transformedBarrelPos.Y); foreach (ParticleEmitter emitter in particleEmitters) @@ -135,6 +142,12 @@ namespace Barotrauma.Items.Components } } + public override void UpdateBroken(float deltaTime, Camera cam) + { + base.UpdateBroken(deltaTime, cam); + recoilTimer -= deltaTime; + } + partial void UpdateProjSpecific(float deltaTime) { recoilTimer -= deltaTime; @@ -232,15 +245,21 @@ namespace Barotrauma.Items.Components float recoilOffset = 0.0f; if (Math.Abs(RecoilDistance) > 0.0f && recoilTimer > 0.0f) { - //move the barrel backwards 0.1 seconds after launching - if (recoilTimer >= Math.Max(Reload, 0.1f) - 0.1f) + float diff = RetractionTime - RecoilTime; + if (recoilTimer >= diff) { - recoilOffset = RecoilDistance * (1.0f - (recoilTimer - (Math.Max(Reload, 0.1f) - 0.1f)) / 0.1f); + //move the barrel backwards 0.1 seconds (defined by RecoilTime) after launching + recoilOffset = RecoilDistance * (1.0f - (recoilTimer - diff) / RecoilTime); + } + else if (recoilTimer <= diff - RetractionDelay) + { + //move back to normal position while reloading + float t = diff - RetractionDelay; + recoilOffset = RecoilDistance * recoilTimer / t; } - //move back to normal position while reloading else { - recoilOffset = RecoilDistance * recoilTimer / (Math.Max(Reload, 0.1f) - 0.1f); + recoilOffset = RecoilDistance; } } @@ -504,7 +523,7 @@ namespace Barotrauma.Items.Components public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { UInt16 projectileID = msg.ReadUInt16(); - float newTargetRotation = msg.ReadRangedSingle(minRotation, maxRotation, 8); + float newTargetRotation = msg.ReadRangedSingle(minRotation, maxRotation, 16); if (Character.Controlled == null || user != Character.Controlled) { @@ -514,13 +533,21 @@ namespace Barotrauma.Items.Components //projectile removed, do nothing if (projectileID == 0) { return; } - if (!(Entity.FindEntityByID(projectileID) is Item projectile)) + //ID ushort.MaxValue = launched without a projectile + if (projectileID == ushort.MaxValue) { - DebugConsole.ThrowError("Failed to launch a projectile - item with the ID \"" + projectileID + " not found"); - return; + Launch(null); + } + else + { + if (!(Entity.FindEntityByID(projectileID) is Item projectile)) + { + DebugConsole.ThrowError("Failed to launch a projectile - item with the ID \"" + projectileID + " not found"); + return; + } + Launch(projectile, launchRotation: newTargetRotation); } - Launch(projectile); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index 370dde2e6..11b3c45b6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -31,7 +31,7 @@ namespace Barotrauma public Sprite SlotSprite; - public Keys QuickUseKey; + public int InventoryKeyIndex = -1; public int SubInventoryDir = -1; @@ -191,7 +191,7 @@ namespace Barotrauma public Item Item; public bool IsSubSlot; public string Tooltip; - public List TooltipColorData; + public List TooltipRichTextData; public SlotReference(Inventory parentInventory, InventorySlot slot, int slotIndex, bool isSubSlot, Inventory subInventory = null) { @@ -201,7 +201,7 @@ namespace Barotrauma Inventory = subInventory; IsSubSlot = isSubSlot; Item = ParentInventory.Items[slotIndex]; - TooltipColorData = ColorData.GetColorData(GetTooltip(Item), out Tooltip); + TooltipRichTextData = RichTextData.GetRichTextData(GetTooltip(Item), out Tooltip); } private string GetTooltip(Item item) @@ -450,12 +450,33 @@ namespace Barotrauma } }*/ - bool mouseOn = interactRect.Contains(PlayerInput.MousePosition) && !Locked && !mouseOnGUI; + bool mouseOn = interactRect.Contains(PlayerInput.MousePosition) && !Locked && !mouseOnGUI && !slot.Disabled; + + // Delete item from container in sub editor + if (SubEditorScreen.IsSubEditor() && PlayerInput.IsCtrlDown()) + { + draggingItem = null; + var mouseDrag = SubEditorScreen.MouseDragStart != Vector2.Zero && Vector2.Distance(PlayerInput.MousePosition, SubEditorScreen.MouseDragStart) >= GUI.Scale * 20; + if (mouseOn && (PlayerInput.PrimaryMouseButtonClicked() || mouseDrag)) + { + if (item != null) + { + if (mouseDrag) { item.OwnInventory?.DeleteAllItems(); } + slot.ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.4f); + if (!mouseDrag) + { + GUI.PlayUISound(GUISoundType.PickItem); + } + item.Remove(); + } + } + } + if (PlayerInput.LeftButtonHeld() && PlayerInput.RightButtonHeld()) { mouseOn = false; } - + if (selectedSlot != null && selectedSlot.Slot != slot) { //subinventory slot highlighted -> don't allow highlighting this one @@ -476,11 +497,14 @@ namespace Barotrauma // && //(highlightedSubInventories.Count == 0 || highlightedSubInventories.Contains(this) || highlightedSubInventorySlot?.Slot == slot || highlightedSubInventory.Owner == item)) { + slot.State = GUIComponent.ComponentState.Hover; if (selectedSlot == null || (!selectedSlot.IsSubSlot && isSubSlot)) { - selectedSlot = new SlotReference(this, slot, slotIndex, isSubSlot, Items[slotIndex]?.GetComponent()?.Inventory); + var slotRef = new SlotReference(this, slot, slotIndex, isSubSlot, Items[slotIndex]?.GetComponent()?.Inventory); + if (Screen.Selected is SubEditorScreen editor && !editor.WiringMode && slotRef.ParentInventory is CharacterInventory) { return; } + selectedSlot = slotRef; } if (draggingItem == null) @@ -668,18 +692,34 @@ namespace Barotrauma DrawSlot(spriteBatch, this, slots[i], Items[i], i, drawItem); } } + + /// + /// Check if the mouse is hovering on top of the slot + /// + /// The desired slot we want to check + /// True if our mouse is hover on the slot, false otherwise + public static bool IsMouseOnSlot(InventorySlot slot) + { + var rect = new Rectangle(slot.InteractRect.X, slot.InteractRect.Y, slot.InteractRect.Width, slot.InteractRect.Height); + rect.Offset(slot.DrawOffset); + return rect.Contains(PlayerInput.MousePosition); + } /// /// Is the mouse on any inventory element (slot, equip button, subinventory...) /// /// - public static bool IsMouseOnInventory() + public static bool IsMouseOnInventory(bool ignoreDraggedItem = false) { - if (Character.Controlled == null) return false; + var isSubEditor = Screen.Selected is SubEditorScreen editor && !editor.WiringMode; + if (Character.Controlled == null) { return false; } - if (draggingItem != null || DraggingInventory != null) return true; + if (!ignoreDraggedItem) + { + if (draggingItem != null || DraggingInventory != null) { return true; } + } - if (Character.Controlled.Inventory != null) + if (Character.Controlled.Inventory != null && !isSubEditor) { var inv = Character.Controlled.Inventory; for (var i = 0; i < inv.slots.Length; i++) @@ -699,7 +739,8 @@ namespace Barotrauma } } } - if (Character.Controlled.SelectedCharacter?.Inventory != null) + + if (Character.Controlled.SelectedCharacter?.Inventory != null && !isSubEditor) { var inv = Character.Controlled.SelectedCharacter.Inventory; for (var i = 0; i < inv.slots.Length; i++) @@ -830,9 +871,9 @@ namespace Barotrauma return CursorState.Default; } - protected static void DrawToolTip(SpriteBatch spriteBatch, string toolTip, Rectangle highlightedSlot, List colorData = null) + protected static void DrawToolTip(SpriteBatch spriteBatch, string toolTip, Rectangle highlightedSlot, List richTextData = null) { - GUIComponent.DrawToolTip(spriteBatch, toolTip, highlightedSlot, colorData); + GUIComponent.DrawToolTip(spriteBatch, toolTip, highlightedSlot, richTextData); } public void DrawSubInventory(SpriteBatch spriteBatch, int slotIndex) @@ -928,7 +969,8 @@ namespace Barotrauma { Character.Controlled.ClearInputs(); - if (CharacterHealth.OpenHealthWindow != null && + if (!IsMouseOnInventory(ignoreDraggedItem: true) && + CharacterHealth.OpenHealthWindow != null && CharacterHealth.OpenHealthWindow.OnItemDropped(draggingItem, false)) { draggingItem = null; @@ -946,8 +988,33 @@ namespace Barotrauma } else { - GUI.PlayUISound(GUISoundType.DropItem); - draggingItem.Drop(Character.Controlled); + bool removed = false; + if (Screen.Selected is SubEditorScreen editor) + { + if (editor.EntityMenu.Rect.Contains(PlayerInput.MousePosition)) + { + draggingItem.Remove(); + removed = true; + } + else + { + if (editor.WiringMode) + { + draggingItem.Remove(); + removed = true; + } + else + { + draggingItem.Drop(Character.Controlled); + } + } + } + else + { + draggingItem.Drop(Character.Controlled); + } + + GUI.PlayUISound(removed ? GUISoundType.PickItem : GUISoundType.DropItem); } } else if (selectedSlot.ParentInventory.Items[selectedSlot.SlotIndex] != draggingItem) @@ -1015,7 +1082,7 @@ namespace Barotrauma protected static Rectangle GetSubInventoryHoverArea(SlotReference subSlot) { Rectangle hoverArea; - if (!subSlot.Inventory.Movable()) + if (!subSlot.Inventory.Movable() || Character.Controlled?.Inventory == subSlot.ParentInventory && !Character.Controlled.HasEquippedItem(subSlot.Item)) { hoverArea = subSlot.Slot.Rect; hoverArea.Location += subSlot.Slot.DrawOffset.ToPoint(); @@ -1038,7 +1105,9 @@ namespace Barotrauma } if (subSlot.Slot.SubInventoryDir < 0) { - hoverArea.Height -= hoverArea.Bottom - subSlot.Slot.Rect.Bottom; + // 24/2/2020 - the below statement makes the sub inventory extend all the way to the bottom of the screen because of a double negative + // Not sure if it's intentional or not but it was causing hover issues and disabling it seems to have no detrimental effects. + // hoverArea.Height -= hoverArea.Bottom - subSlot.Slot.Rect.Bottom; } else { @@ -1088,7 +1157,7 @@ namespace Barotrauma string toolTip = mouseOnHealthInterface ? TextManager.Get("QuickUseAction.UseTreatment") : Character.Controlled.FocusedItem != null ? TextManager.GetWithVariable("PutItemIn", "[itemname]", Character.Controlled.FocusedItem.Name, true) : - TextManager.Get("DropItem"); + TextManager.Get(Screen.Selected is SubEditorScreen editor && editor.EntityMenu.Rect.Contains(PlayerInput.MousePosition) ? "Delete" : "DropItem"); int textWidth = (int)Math.Max(GUI.Font.MeasureString(draggingItem.Name).X, GUI.SmallFont.MeasureString(toolTip).X); int textSpacing = (int)(15 * GUI.Scale); Point shadowBorders = (new Point(40, 10)).Multiply(GUI.Scale); @@ -1111,7 +1180,7 @@ namespace Barotrauma { Rectangle slotRect = selectedSlot.Slot.Rect; slotRect.Location += selectedSlot.Slot.DrawOffset.ToPoint(); - DrawToolTip(spriteBatch, selectedSlot.Tooltip, slotRect, selectedSlot.TooltipColorData); + DrawToolTip(spriteBatch, selectedSlot.Tooltip, slotRect, selectedSlot.TooltipRichTextData); } } @@ -1154,6 +1223,11 @@ namespace Barotrauma if (inventory != null && inventory.Locked) { slotColor = Color.Gray * 0.5f; } spriteBatch.Draw(slotSprite.Texture, rect, slotSprite.SourceRect, slotColor); + + if (SubEditorScreen.IsSubEditor() && PlayerInput.IsCtrlDown() && selectedSlot?.Slot == slot) + { + GUI.DrawRectangle(spriteBatch, rect, GUI.Style.Red * 0.3f, isFilled: true); + } bool canBePut = false; @@ -1181,7 +1255,7 @@ namespace Barotrauma if (item != null && drawItem) { - if (!item.IsFullCondition && (itemContainer == null || !itemContainer.ShowConditionInContainedStateIndicator)) + if (!item.IsFullCondition && !item.Prefab.HideConditionBar && (itemContainer == null || !itemContainer.ShowConditionInContainedStateIndicator)) { GUI.DrawRectangle(spriteBatch, new Rectangle(rect.X, rect.Bottom - 8, rect.Width, 8), Color.Black * 0.8f, true); GUI.DrawRectangle(spriteBatch, @@ -1308,10 +1382,10 @@ namespace Barotrauma if (inventory != null && !inventory.Locked && Character.Controlled?.Inventory == inventory && - slot.QuickUseKey != Keys.None) + slot.InventoryKeyIndex != -1) { spriteBatch.Draw(slotHotkeySprite.Texture, rect.ScaleSize(1.15f), slotHotkeySprite.SourceRect, slotColor); - GUI.DrawString(spriteBatch, rect.Location.ToVector2() + new Vector2((int)(4.25f * UIScale), (int)Math.Ceiling(-1.5f * UIScale)), slot.QuickUseKey.ToString().Substring(1, 1), Color.Black, font: GUI.HotkeyFont); + GUI.DrawString(spriteBatch, rect.Location.ToVector2() + new Vector2((int)(4.25f * UIScale), (int)Math.Ceiling(-1.5f * UIScale)), GameMain.Config.InventoryKeyBind(slot.InventoryKeyIndex).Name, Color.Black, font: GUI.HotkeyFont); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 4f5d60369..afb63312a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -32,17 +32,28 @@ namespace Barotrauma private readonly Dictionary spriteAnimState = new Dictionary(); + public bool FakeBroken; + private Sprite activeSprite; public override Sprite Sprite { get { return activeSprite; } } - public override bool DrawOverWater + public override Rectangle Rect { - get { return base.DrawOverWater || (GetComponent() != null && IsSelected); } + get { return base.Rect; } + set + { + cachedVisibleSize = null; + base.Rect = value; + } } + public override bool DrawBelowWater => (!(Screen.Selected is SubEditorScreen editor) || !editor.WiringMode || !isWire) && base.DrawBelowWater; + + public override bool DrawOverWater => base.DrawOverWater || (IsSelected || Screen.Selected is SubEditorScreen editor && editor.WiringMode) && isWire; + private GUITextBlock itemInUseWarning; private GUITextBlock ItemInUseWarning { @@ -62,6 +73,10 @@ namespace Barotrauma { get { + if (!GameMain.SubEditorScreen.ShowThalamus && prefab.Category.HasFlag(MapEntityCategory.Thalamus)) + { + return false; + } return parentInventory == null && (body == null || body.Enabled) && ShowItems; } } @@ -156,6 +171,14 @@ namespace Barotrauma decorativeSprite.Sprite.EnsureLazyLoaded(); spriteAnimState.Add(decorativeSprite, new DecorativeSprite.State()); } + UpdateSpriteStates(0.0f); + } + + private Vector2? cachedVisibleSize; + + public void ResetCachedVisibleSize() + { + cachedVisibleSize = null; } public override bool IsVisible(Rectangle worldView) @@ -167,19 +190,28 @@ namespace Barotrauma } //no drawable components and the body has been disabled = nothing to draw - if (drawableComponents.Count == 0 && body != null && !body.Enabled) + if (!hasComponentsToDraw && body != null && !body.Enabled) { return false; } - float padding = 100.0f; - Vector2 size = new Vector2(rect.Width + padding, rect.Height + padding); - foreach (IDrawableComponent drawable in drawableComponents) + Vector2 size; + if (cachedVisibleSize.HasValue) { - size.X = Math.Max(drawable.DrawSize.X, size.X); - size.Y = Math.Max(drawable.DrawSize.Y, size.Y); + size = cachedVisibleSize.Value; + } + else + { + float padding = 100.0f; + size = new Vector2(rect.Width + padding, rect.Height + padding); + foreach (IDrawableComponent drawable in drawableComponents) + { + size.X = Math.Max(drawable.DrawSize.X, size.X); + size.Y = Math.Max(drawable.DrawSize.Y, size.Y); + } + size *= 0.5f; + cachedVisibleSize = size; } - size *= 0.5f; //cache world position so we don't need to calculate it 4 times Vector2 worldPosition = WorldPosition; @@ -199,7 +231,8 @@ namespace Barotrauma BrokenItemSprite fadeInBrokenSprite = null; float fadeInBrokenSpriteAlpha = 0.0f; - if (condition < Prefab.Health) + float displayCondition = FakeBroken ? 0.0f : condition; + if (displayCondition < Prefab.Health) { for (int i = 0; i < Prefab.BrokenSprites.Count; i++) { @@ -207,14 +240,14 @@ namespace Barotrauma { float min = i > 0 ? Prefab.BrokenSprites[i - i].MaxCondition : 0.0f; float max = Prefab.BrokenSprites[i].MaxCondition; - fadeInBrokenSpriteAlpha = 1.0f - ((condition - min) / (max - min)); + fadeInBrokenSpriteAlpha = 1.0f - ((displayCondition - min) / (max - min)); if (fadeInBrokenSpriteAlpha > 0.0f && fadeInBrokenSpriteAlpha < 1.0f) { fadeInBrokenSprite = Prefab.BrokenSprites[i]; } continue; } - if (condition <= Prefab.BrokenSprites[i].MaxCondition) + if (displayCondition <= Prefab.BrokenSprites[i].MaxCondition) { activeSprite = Prefab.BrokenSprites[i].Sprite; break; @@ -262,7 +295,7 @@ namespace Barotrauma float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState); Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState) * Scale; decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color, - SpriteRotation + rotation, Scale, activeSprite.effects, + SpriteRotation + rotation, decorativeSprite.Scale * Scale, activeSprite.effects, depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth), 0.999f)); } } @@ -306,7 +339,7 @@ namespace Barotrauma Vector2 transformedOffset = new Vector2(ca * offset.X + sa * offset.Y, -sa * offset.X + ca * offset.Y); decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + transformedOffset.X, -(DrawPosition.Y + transformedOffset.Y)), color, - -body.Rotation + rotation, Scale, activeSprite.effects, + -body.Rotation + rotation, decorativeSprite.Scale * Scale, activeSprite.effects, depth: depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth)); } } @@ -671,6 +704,13 @@ namespace Barotrauma HUDLayoutSettings.ChatBoxArea.Width + disallowedPadding, HUDLayoutSettings.ChatBoxArea.Height)); } + if (Screen.Selected is SubEditorScreen editor) + { + disallowedAreas.Add(editor.EntityMenu.Rect); + disallowedAreas.Add(editor.TopPanel.Rect); + disallowedAreas.Add(editor.ToggleEntityMenuButton.Rect); + } + GUI.PreventElementOverlap(elementsToMove, disallowedAreas, new Rectangle( 0, 20, @@ -826,19 +866,21 @@ namespace Barotrauma } readonly List texts = new List(); - public List GetHUDTexts(Character character) + public List GetHUDTexts(Character character, bool recreateHudTexts = true) { + // Always create the texts if they have not yet been created + if (texts.Any() && !recreateHudTexts) { return texts; } texts.Clear(); foreach (ItemComponent ic in components) { - if (string.IsNullOrEmpty(ic.DisplayMsg)) continue; - if (!ic.CanBePicked && !ic.CanBeSelected) continue; - if (ic is Holdable holdable && !holdable.CanBeDeattached()) continue; + if (string.IsNullOrEmpty(ic.DisplayMsg)) { continue; } + if (!ic.CanBePicked && !ic.CanBeSelected) { continue; } + if (ic is Holdable holdable && !holdable.CanBeDeattached()) { continue; } Color color = Color.Gray; if (ic.HasRequiredItems(character, false)) { - if (ic is Repairable repairable) + if (ic is Repairable) { if (!IsFullCondition) { color = Color.Cyan; } } @@ -847,9 +889,12 @@ namespace Barotrauma color = Color.Cyan; } } - texts.Add(new ColoredText(ic.DisplayMsg, color, false)); } + if ((PlayerInput.KeyDown(Keys.LeftShift) || PlayerInput.KeyDown(Keys.RightShift)) && CrewManager.DoesItemHaveContextualOrders(this)) + { + texts.Add(new ColoredText(TextManager.ParseInputTypes(TextManager.Get("itemmsgcontextualorders")), Color.Cyan, false)); + } return texts; } @@ -857,17 +902,17 @@ namespace Barotrauma { if (Screen.Selected is SubEditorScreen) { - if (editingHUD != null && editingHUD.UserData == this) editingHUD.AddToGUIUpdateList(); + if (editingHUD != null && editingHUD.UserData == this) { editingHUD.AddToGUIUpdateList(); } } else { if (HasInGameEditableProperties) { - if (editingHUD != null && editingHUD.UserData == this) editingHUD.AddToGUIUpdateList(); + if (editingHUD != null && editingHUD.UserData == this) { editingHUD.AddToGUIUpdateList(); } } } - if (Character.Controlled != null && Character.Controlled?.SelectedConstruction != this) return; + if (Character.Controlled != null && Character.Controlled?.SelectedConstruction != this) { return; } bool needsLayoutUpdate = false; foreach (ItemComponent ic in activeHUDs) @@ -1213,8 +1258,8 @@ namespace Barotrauma { if (itemContainerIndex < 0 || itemContainerIndex >= parentItem.components.Count) { - string errorMsg = "Failed to spawn item \"" + (itemIdentifier ?? "null") + - "\" in the inventory of \"" + parentItem.prefab.Identifier + "\" (component index out of range). Index: " + itemContainerIndex + ", components: " + parentItem.components.Count + "."; + string errorMsg = + $"Failed to spawn item \"{(itemIdentifier ?? "null")}\" in the inventory of \"{parentItem.prefab.Identifier} ({parentItem.ID})\" (component index out of range). Index: {itemContainerIndex}, components: {parentItem.components.Count}."; GameAnalyticsManager.AddErrorEventOnce("Item.ReadSpawnData:ContainerIndexOutOfRange" + (itemName ?? "null") + (itemIdentifier ?? "null"), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs index 04bd09373..1b11c5ead 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs @@ -66,16 +66,17 @@ namespace Barotrauma [Serialize("", false)] public string ImpactSoundTag { get; private set; } - public override void UpdatePlacing(Camera cam) { Vector2 position = Submarine.MouseToWorldGrid(cam, Submarine.MainSub); - + if (PlayerInput.SecondaryMouseButtonClicked()) { selected = null; return; } + + var potentialContainer = MapEntity.GetPotentialContainer(position); if (!ResizeHorizontal && !ResizeVertical) { @@ -88,6 +89,14 @@ namespace Barotrauma item.SetTransform(ConvertUnits.ToSimUnits(Submarine.MainSub == null ? item.Position : item.Position - Submarine.MainSub.Position), 0.0f); item.FindHull(); + if (PlayerInput.IsShiftDown()) + { + if (potentialContainer?.OwnInventory?.TryPutItem(item, Character.Controlled) ?? false) + { + GUI.PlayUISound(GUISoundType.PickItem); + } + } + placePosition = Vector2.Zero; return; } @@ -124,6 +133,12 @@ namespace Barotrauma } } + if (potentialContainer != null) + { + potentialContainer.IsHighlighted = true; + } + + //if (PlayerInput.GetMouseState.RightButton == ButtonState.Pressed) selected = null; } @@ -141,26 +156,10 @@ namespace Barotrauma if (!ResizeHorizontal && !ResizeVertical) { sprite.Draw(spriteBatch, new Vector2(position.X, -position.Y) + sprite.size / 2.0f * Scale, SpriteColor, scale: Scale); - } else { - Vector2 placeSize = size; - if (placePosition == Vector2.Zero) - { - if (PlayerInput.PrimaryMouseButtonHeld()) placePosition = position; - } - else - { - if (ResizeHorizontal) - placeSize.X = Math.Max(position.X - placePosition.X, size.X); - if (ResizeVertical) - placeSize.Y = Math.Max(placePosition.Y - position.Y, size.Y); - - position = placePosition; - } - - if (sprite != null) sprite.DrawTiled(spriteBatch, new Vector2(position.X, -position.Y), placeSize, color: SpriteColor); + sprite?.DrawTiled(spriteBatch, new Vector2(position.X, -position.Y), size, color: SpriteColor); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/FireSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/FireSource.cs index 03751e45e..c007fc16f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/FireSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/FireSource.cs @@ -60,10 +60,10 @@ namespace Barotrauma var particle = GameMain.ParticleManager.CreateParticle("flame", particlePos, particleVel, 0.0f, hull); - if (particle == null) continue; + if (particle == null) { continue; } //make some of the particles create another firesource when they enter another hull - if (Rand.Int(20) == 1) particle.OnChangeHull = onChangeHull; + if (Rand.Int(20) == 1) { particle.OnChangeHull = onChangeHull; } particle.Size *= MathHelper.Clamp(size.X / 60.0f * Math.Max(hull.Oxygen / hull.Volume, 0.4f), 0.5f, 1.0f); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs index ac8c01edf..c9101a9fc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs @@ -2,6 +2,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; +using System.Linq; namespace Barotrauma { @@ -24,7 +25,7 @@ namespace Barotrauma public override void Draw(SpriteBatch sb, bool editing, bool back = true) { - if (!GameMain.DebugDraw && Screen.Selected.Cam.Zoom > 0.1f) + if (GameMain.DebugDraw && Screen.Selected.Cam.Zoom > 0.1f) { Vector2 center = new Vector2(WorldRect.X + rect.Width / 2.0f, -(WorldRect.Y - rect.Height / 2.0f)); GUI.DrawLine(sb, center, center + new Vector2(flowForce.X, -flowForce.Y) / 10.0f, GUI.Style.Red); @@ -121,24 +122,36 @@ namespace Barotrauma partial void EmitParticles(float deltaTime) { - if (flowTargetHull == null) return; - + if (flowTargetHull == null) { return; } + + if (linkedTo.Count == 2 && linkedTo[0] is Hull hull1 && linkedTo[1] is Hull hull2) + { + //no flow particles between linked hulls (= rooms consisting of multiple hulls) + if (hull1.linkedTo.Contains(hull2)) { return; } + if (hull1.linkedTo.Any(h => h.linkedTo.Contains(hull1) && h.linkedTo.Contains(hull2))) { return; } + if (hull2.linkedTo.Any(h => h.linkedTo.Contains(hull1) && h.linkedTo.Contains(hull2))) { return; } + } + Vector2 pos = Position; if (IsHorizontal) { pos.X += Math.Sign(flowForce.X); - pos.Y = MathHelper.Clamp((higherSurface + lowerSurface) / 2.0f, rect.Y - rect.Height, rect.Y) + 10; + pos.Y = MathHelper.Clamp(Rand.Range(higherSurface, lowerSurface), rect.Y - rect.Height, rect.Y); } else { pos.Y += Math.Sign(flowForce.Y) * rect.Height / 2.0f; } + //spawn less particles when there's already a large number of them + float particleAmountMultiplier = 1.0f - GameMain.ParticleManager.ParticleCount / (float)GameMain.ParticleManager.MaxParticles; + particleAmountMultiplier *= particleAmountMultiplier; + //light dripping if (open < 0.2f && LerpedFlowForce.LengthSquared() > 100.0f) { particleTimer += deltaTime; - float particlesPerSec = open * 100.0f; + float particlesPerSec = open * 100.0f * particleAmountMultiplier; float emitInterval = 1.0f / particlesPerSec; while (particleTimer > emitInterval) { @@ -174,12 +187,13 @@ namespace Barotrauma particleTimer += deltaTime; if (IsHorizontal) { - float particlesPerSec = open * rect.Height * 0.1f; + float particlesPerSec = open * rect.Height * 0.1f * particleAmountMultiplier; + if (openedTimer > 0.0f) { particlesPerSec *= 1.0f + openedTimer * 10.0f; } float emitInterval = 1.0f / particlesPerSec; while (particleTimer > emitInterval) { Vector2 velocity = new Vector2( - MathHelper.Clamp(flowForce.X, -5000.0f, 5000.0f) * Rand.Range(0.5f, 0.7f), + MathHelper.Clamp(flowForce.X, -5000.0f, 5000.0f) * Rand.Range(0.5f, 0.7f), flowForce.Y * Rand.Range(0.5f, 0.7f)); if (flowTargetHull.WaterVolume < flowTargetHull.Volume * 0.95f) @@ -191,11 +205,11 @@ namespace Barotrauma if (particle != null) { - particle.Size = particle.Size * Math.Min(Math.Abs(flowForce.X / 1000.0f), 5.0f); + particle.Size *= Math.Min(Math.Abs(flowForce.X / 500.0f), 5.0f); } } - if (Math.Abs(flowForce.X) > 300.0f) + if (Math.Abs(flowForce.X) > 300.0f && flowTargetHull.WaterVolume > flowTargetHull.Volume * 0.1f) { pos.X += Math.Sign(flowForce.X) * 10.0f; if (rect.Height < 32) @@ -211,7 +225,7 @@ namespace Barotrauma GameMain.ParticleManager.CreateParticle( "bubbles", Submarine == null ? pos : pos + Submarine.Position, - flowForce / 10.0f, 0, flowTargetHull); + velocity, 0, flowTargetHull); } particleTimer -= emitInterval; } @@ -220,7 +234,7 @@ namespace Barotrauma { if (Math.Sign(flowTargetHull.Rect.Y - rect.Y) != Math.Sign(lerpedFlowForce.Y)) return; - float particlesPerSec = open * rect.Width * 0.3f; + float particlesPerSec = open * rect.Width * 0.3f * particleAmountMultiplier; float emitInterval = 1.0f / particlesPerSec; while (particleTimer > emitInterval) { @@ -237,7 +251,7 @@ namespace Barotrauma velocity, 0, FlowTargetHull); if (splash != null) splash.Size = splash.Size * MathHelper.Clamp(rect.Width / 50.0f, 0.8f, 4.0f); } - if (Math.Abs(flowForce.Y) > 190.0f && Rand.Range(0.0f, 1.0f) < 0.3f) + if (Math.Abs(flowForce.Y) > 190.0f && Rand.Range(0.0f, 1.0f) < 0.3f && flowTargetHull.WaterVolume > flowTargetHull.Volume * 0.1f) { GameMain.ParticleManager.CreateParticle( "bubbles", diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index 219996222..69debe79f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -14,7 +14,7 @@ namespace Barotrauma { public const int MaxDecalsPerHull = 10; - private List decals = new List(); + private readonly List decals = new List(); private float serverUpdateDelay; private float remoteWaterVolume, remoteOxygenPercentage; @@ -23,6 +23,8 @@ namespace Barotrauma private bool networkUpdatePending; private float networkUpdateTimer; + private double lastAmbientLightEditTime; + public override bool SelectableInEditor { get @@ -232,33 +234,36 @@ namespace Barotrauma return; } - /*if (!Visible) + if (!ShowHulls && !GameMain.DebugDraw) { return; } + + if (!editing && (!GameMain.DebugDraw || Screen.Selected.Cam.Zoom < 0.1f)) { return; } + + float alpha = 1.0f; + float hideTimeAfterEdit = 3.0f; + if (lastAmbientLightEditTime > Timing.TotalTime - hideTimeAfterEdit * 2.0f) { - drawRect = - Submarine == null ? rect : new Rectangle((int)(Submarine.DrawPosition.X + rect.X), (int)(Submarine.DrawPosition.Y + rect.Y), rect.Width, rect.Height); - - GUI.DrawRectangle(spriteBatch, - new Vector2(drawRect.X, -drawRect.Y), - new Vector2(rect.Width, rect.Height), - Color.Black, true, - 0, (int)Math.Max((1.5f / GameScreen.Selected.Cam.Zoom), 1.0f)); - }*/ - - if (!ShowHulls && !GameMain.DebugDraw) return; - - if (!editing && (!GameMain.DebugDraw || Screen.Selected.Cam.Zoom < 0.1f)) return; + alpha = Math.Min((float)(Timing.TotalTime - lastAmbientLightEditTime) / hideTimeAfterEdit - 1.0f, 1.0f); + } Rectangle drawRect = Submarine == null ? rect : new Rectangle((int)(Submarine.DrawPosition.X + rect.X), (int)(Submarine.DrawPosition.Y + rect.Y), rect.Width, rect.Height); + if ((IsSelected || IsHighlighted) && editing) + { + GUI.DrawRectangle(spriteBatch, + new Vector2(drawRect.X, -drawRect.Y), + new Vector2(rect.Width, rect.Height), + (IsHighlighted ? Color.LightBlue * 0.8f : GUI.Style.Red * 0.5f) * alpha, false, 0, (int)Math.Max(5.0f / Screen.Selected.Cam.Zoom, 1.0f)); + } + GUI.DrawRectangle(spriteBatch, new Vector2(drawRect.X, -drawRect.Y), new Vector2(rect.Width, rect.Height), - Color.Blue, false, (ID % 255) * 0.000001f, (int)Math.Max((1.5f / Screen.Selected.Cam.Zoom), 1.0f)); + Color.Blue * alpha, false, (ID % 255) * 0.000001f, (int)Math.Max(1.5f / Screen.Selected.Cam.Zoom, 1.0f)); GUI.DrawRectangle(spriteBatch, new Rectangle(drawRect.X, -drawRect.Y, rect.Width, rect.Height), - GUI.Style.Red * ((100.0f - OxygenPercentage) / 400.0f), true, 0, (int)Math.Max((1.5f / GameScreen.Selected.Cam.Zoom), 1.0f)); + GUI.Style.Red * ((100.0f - OxygenPercentage) / 400.0f) * alpha, true, 0, (int)Math.Max(1.5f / Screen.Selected.Cam.Zoom, 1.0f)); if (GameMain.DebugDraw) { @@ -277,10 +282,12 @@ namespace Barotrauma foreach (FireSource fs in FireSources) { Rectangle fireSourceRect = new Rectangle((int)fs.WorldPosition.X, -(int)fs.WorldPosition.Y, (int)fs.Size.X, (int)fs.Size.Y); - GUI.DrawRectangle(spriteBatch, fireSourceRect, GUI.Style.Orange, false, 0, 5); + GUI.DrawRectangle(spriteBatch, fireSourceRect, GUI.Style.Red, false, 0, 5); + GUI.DrawRectangle(spriteBatch, new Rectangle(fireSourceRect.X - (int)fs.DamageRange, fireSourceRect.Y, fireSourceRect.Width + (int)fs.DamageRange * 2, fireSourceRect.Height), GUI.Style.Orange, false, 0, 5); //GUI.DrawRectangle(spriteBatch, new Rectangle((int)fs.LastExtinguishPos.X, (int)-fs.LastExtinguishPos.Y, 5,5), Color.Yellow, true); } + /*GUI.DrawLine(spriteBatch, new Vector2(drawRect.X, -WorldSurface), new Vector2(drawRect.Right, -WorldSurface), Color.Cyan * 0.5f); for (int i = 0; i < waveY.Length - 1; i++) { @@ -290,24 +297,15 @@ namespace Barotrauma }*/ } - if ((IsSelected || IsHighlighted) && editing) - { - GUI.DrawRectangle(spriteBatch, - new Vector2(drawRect.X + 5, -drawRect.Y + 5), - new Vector2(rect.Width - 10, rect.Height - 10), - IsHighlighted ? Color.LightBlue * 0.5f : GUI.Style.Red * 0.5f, true, 0, (int)Math.Max((1.5f / GameScreen.Selected.Cam.Zoom), 1.0f)); - } - foreach (MapEntity e in linkedTo) { - if (e is Hull) + if (e is Hull linkedHull) { - Hull linkedHull = (Hull)e; - Rectangle connectedHullRect = e.Submarine == null ? - linkedHull.rect : + Rectangle connectedHullRect = e.Submarine == null ? + linkedHull.rect : new Rectangle( (int)(Submarine.DrawPosition.X + linkedHull.WorldPosition.X), - (int)(Submarine.DrawPosition.Y + linkedHull.WorldPosition.Y), + (int)(Submarine.DrawPosition.Y + linkedHull.WorldPosition.Y), linkedHull.WorldRect.Width, linkedHull.WorldRect.Height); //center of the hull @@ -315,7 +313,7 @@ namespace Barotrauma WorldRect : new Rectangle( (int)(Submarine.DrawPosition.X + WorldPosition.X), - (int)(Submarine.DrawPosition.Y + WorldPosition.Y), + (int)(Submarine.DrawPosition.Y + WorldPosition.Y), WorldRect.Width, WorldRect.Height); GUI.DrawLine(spriteBatch, @@ -326,22 +324,22 @@ namespace Barotrauma } } - public static void UpdateVertices(GraphicsDevice graphicsDevice, Camera cam, WaterRenderer renderer) + public static void UpdateVertices(Camera cam, WaterRenderer renderer) { foreach (EntityGrid entityGrid in EntityGrids) { - if (entityGrid.WorldRect.X > cam.WorldView.Right || entityGrid.WorldRect.Right < cam.WorldView.X) continue; - if (entityGrid.WorldRect.Y - entityGrid.WorldRect.Height > cam.WorldView.Y || entityGrid.WorldRect.Y < cam.WorldView.Y - cam.WorldView.Height) continue; + if (entityGrid.WorldRect.X > cam.WorldView.Right || entityGrid.WorldRect.Right < cam.WorldView.X) { continue; } + if (entityGrid.WorldRect.Y - entityGrid.WorldRect.Height > cam.WorldView.Y || entityGrid.WorldRect.Y < cam.WorldView.Y - cam.WorldView.Height) { continue; } var allEntities = entityGrid.GetAllEntities(); foreach (Hull hull in allEntities) { - hull.UpdateVertices(graphicsDevice, cam, entityGrid, renderer); + hull.UpdateVertices(cam, entityGrid, renderer); } } } - private void UpdateVertices(GraphicsDevice graphicsDevice, Camera cam, EntityGrid entityGrid, WaterRenderer renderer) + private void UpdateVertices(Camera cam, EntityGrid entityGrid, WaterRenderer renderer) { Vector2 submarinePos = Submarine == null ? Vector2.Zero : Submarine.DrawPosition; @@ -451,7 +449,7 @@ namespace Barotrauma } //we only create a new quad if this is the first or the last one, of if there's a wave large enough that we need more geometry - if (i == end - 1 || i == start || Math.Abs(prevCorners[1].Y - corners[3].Y) > 1.0f) + if (i == end - 1 || i == start || Math.Abs(prevCorners[1].Y - corners[2].Y) > 0.01f) { renderer.vertices[renderer.PositionInBuffer] = new VertexPositionTexture(prevCorners[0], prevUVs[0]); renderer.vertices[renderer.PositionInBuffer + 1] = new VertexPositionTexture(corners[1], uvCoords[1]); @@ -554,11 +552,10 @@ namespace Barotrauma remoteOxygenPercentage = message.ReadRangedSingle(0.0f, 100.0f, 8); bool hasFireSources = message.ReadBoolean(); - int fireSourceCount = 0; remoteFireSources = new List(); if (hasFireSources) { - fireSourceCount = message.ReadRangedInteger(0, 16); + int fireSourceCount = message.ReadRangedInteger(0, 16); for (int i = 0; i < fireSourceCount; i++) { remoteFireSources.Add(new Vector3( diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs index 0fcff3a6d..f20bd6797 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs @@ -18,6 +18,7 @@ namespace Barotrauma foreach (Pair entity in DisplayEntities) { + if (entity.First is CoreEntityPrefab) { continue; } Rectangle drawRect = entity.Second; drawRect = new Rectangle( (int)(drawRect.X * scale) + drawArea.Center.X, (int)((drawRect.Y) * scale) - drawArea.Center.Y, @@ -33,7 +34,9 @@ namespace Barotrauma foreach (Pair entity in DisplayEntities) { Rectangle drawRect = entity.Second; - drawRect.Location += Submarine.MouseToWorldGrid(cam, Submarine.MainSub).ToPoint(); + + drawRect.Location += placePosition != Vector2.Zero ? placePosition.ToPoint() : Submarine.MouseToWorldGrid(cam, Submarine.MainSub).ToPoint(); + entity.First.DrawPlacing(spriteBatch, drawRect, entity.First.Scale); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs index 7913a822c..f837b4bad 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs @@ -91,11 +91,11 @@ namespace Barotrauma public void RenderWater(SpriteBatch spriteBatch, RenderTarget2D texture, Camera cam) { spriteBatch.GraphicsDevice.BlendState = BlendState.NonPremultiplied; - + WaterEffect.Parameters["xTexture"].SetValue(texture); Vector2 distortionStrength = cam == null ? DistortionStrength : DistortionStrength * cam.Zoom; - WaterEffect.Parameters["xWaveWidth"].SetValue(DistortionStrength.X); - WaterEffect.Parameters["xWaveHeight"].SetValue(DistortionStrength.Y); + WaterEffect.Parameters["xWaveWidth"].SetValue(distortionStrength.X); + WaterEffect.Parameters["xWaveHeight"].SetValue(distortionStrength.Y); if (BlurAmount > 0.0f) { WaterEffect.CurrentTechnique = WaterEffect.Techniques["WaterShaderBlurred"]; @@ -111,6 +111,9 @@ namespace Barotrauma offset += (cam.Position - new Vector2(cam.WorldView.Width / 2.0f, -cam.WorldView.Height / 2.0f)); offset.Y += cam.WorldView.Height; offset.X += cam.WorldView.Width; +#if LINUX || OSX + offset.X += cam.WorldView.Width; +#endif offset *= DistortionScale; } offset.Y = -offset.Y; @@ -176,6 +179,9 @@ namespace Barotrauma spriteBatch.GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, subVerts.Value, 0, PositionInIndoorsBuffer[subVerts.Key] / 3); } + + WaterEffect.Parameters["xTexture"].SetValue((Texture2D)null); + WaterEffect.CurrentTechnique.Passes[0].Apply(); } public void ScrollWater(Vector2 vel, float deltaTime) @@ -195,7 +201,10 @@ namespace Barotrauma basicEffect.CurrentTechnique.Passes[0].Apply(); graphicsDevice.SamplerStates[0] = SamplerState.PointWrap; - graphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, vertices, 0, PositionInBuffer / 3); + graphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, vertices, 0, PositionInBuffer / 3); + + basicEffect.Texture = null; + basicEffect.CurrentTechnique.Passes[0].Apply(); } public void ResetBuffers() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs index 1cbf20833..c099e6c39 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs @@ -93,9 +93,9 @@ namespace Barotrauma.Lights public static BasicEffect shadowEffect; public static BasicEffect penumbraEffect; - private Segment[] segments = new Segment[4]; - private SegmentPoint[] vertices = new SegmentPoint[4]; - private SegmentPoint[] losVertices = new SegmentPoint[4]; + private readonly Segment[] segments = new Segment[4]; + private readonly SegmentPoint[] vertices = new SegmentPoint[4]; + private readonly SegmentPoint[] losVertices = new SegmentPoint[4]; private readonly bool[] backFacing; private readonly bool[] ignoreEdge; @@ -106,6 +106,8 @@ namespace Barotrauma.Lights public VertexPositionTexture[] PenumbraVertices { get; private set; } public int ShadowVertexCount { get; private set; } + private readonly HashSet overlappingHulls = new HashSet(); + public MapEntity ParentEntity { get; private set; } private bool enabled; @@ -176,7 +178,7 @@ namespace Barotrauma.Lights if (door != null) { isHorizontal = door.IsHorizontal; } } - var chList = HullLists.Find(x => x.Submarine == parent.Submarine); + var chList = HullLists.Find(h => h.Submarine == parent.Submarine); if (chList == null) { chList = new ConvexHullList(parent.Submarine); @@ -194,10 +196,12 @@ namespace Barotrauma.Lights private void MergeOverlappingSegments(ConvexHull ch) { - if (ch == this) return; - + if (ch == this) { return; } + if (isHorizontal == ch.isHorizontal) { + if (BoundingBox == ch.BoundingBox) { return; } + //hide segments that are roughly at the some position as some other segment (e.g. the ends of two adjacent wall pieces) float mergeDist = 32; float mergeDistSqr = mergeDist * mergeDist; @@ -206,6 +210,7 @@ namespace Barotrauma.Lights for (int j = 0; j < ch.segments.Length; j++) { if (segments[i].IsHorizontal != ch.segments[j].IsHorizontal) { continue; } + if (ignoreEdge[i] || ch.ignoreEdge[j]) { continue; } //the segments must be at different sides of the convex hulls to be merged //(e.g. the right edge of a wall piece and the left edge of another one) @@ -247,6 +252,7 @@ namespace Barotrauma.Lights p.Y >= ch.BoundingBox.Y && p.Y <= ch.BoundingBox.Bottom) { ignoreEdge[i] = true; + overlappingHulls.Add(ch); } } } @@ -283,11 +289,25 @@ namespace Barotrauma.Lights } else { - losVertices[startPointIndex].Pos = segment2.ConvexHull.losVertices[startPoint2Index].Pos = - (segment1.Start.Pos + segment2.End.Pos) / 2.0f; - losVertices[endPointIndex].Pos = segment2.ConvexHull.losVertices[endPoint2Index].Pos = - (segment1.End.Pos + segment2.Start.Pos) / 2.0f; + if (Vector2.DistanceSquared(losVertices[startPointIndex].Pos, segment1.Start.Pos) < + Vector2.DistanceSquared(losVertices[startPointIndex].Pos, segment1.End.Pos)) + { + losVertices[startPointIndex].Pos = segment2.ConvexHull.losVertices[startPoint2Index].Pos = + (segment1.Start.Pos + segment2.End.Pos) / 2.0f; + losVertices[endPointIndex].Pos = segment2.ConvexHull.losVertices[endPoint2Index].Pos = + (segment1.End.Pos + segment2.Start.Pos) / 2.0f; + } + else + { + losVertices[startPointIndex].Pos = segment2.ConvexHull.losVertices[startPoint2Index].Pos = + (segment1.End.Pos + segment2.Start.Pos) / 2.0f; + losVertices[endPointIndex].Pos = segment2.ConvexHull.losVertices[endPoint2Index].Pos = + (segment1.Start.Pos + segment2.End.Pos) / 2.0f; + } } + + overlappingHulls.Add(segment2.ConvexHull); + segment2.ConvexHull.overlappingHulls.Add(this); } public void Rotate(Vector2 origin, float amount) @@ -328,7 +348,26 @@ namespace Barotrauma.Lights LastVertexChangeTime = (float)Timing.TotalTime; + overlappingHulls.Clear(); + for (int i = 0; i < 4; i++) + { + ignoreEdge[i] = false; + } + CalculateDimensions(); + + if (ParentEntity == null) { return; } + + var chList = HullLists.Find(h => h.Submarine == ParentEntity.Submarine); + if (chList != null) + { + overlappingHulls.Clear(); + foreach (ConvexHull ch in chList.List) + { + MergeOverlappingSegments(ch); + ch.MergeOverlappingSegments(this); + } + } } public void SetVertices(Vector2[] points, Matrix? rotationMatrix = null) @@ -348,6 +387,8 @@ namespace Barotrauma.Lights ignoreEdge[i] = false; } + overlappingHulls.Clear(); + int margin = 0; if (Math.Abs(points[0].X - points[2].X) < Math.Abs(points[0].Y - points[2].Y)) { @@ -381,9 +422,10 @@ namespace Barotrauma.Lights if (ParentEntity == null) return; - var chList = HullLists.Find(x => x.Submarine == ParentEntity.Submarine); + var chList = HullLists.Find(h => h.Submarine == ParentEntity.Submarine); if (chList != null) { + overlappingHulls.Clear(); foreach (ConvexHull ch in chList.List) { MergeOverlappingSegments(ch); @@ -484,8 +526,8 @@ namespace Barotrauma.Lights //find beginning and ending vertices which //belong to the shadow - int startingIndex = 0; - int endingIndex = 0; + int startingIndex = -1; + int endingIndex = -1; for (int i = 0; i < 4; i++) { int currentEdge = i; @@ -498,6 +540,8 @@ namespace Barotrauma.Lights startingIndex = nextEdge; } + if (startingIndex == -1 || endingIndex == -1) { return; } + //nr of vertices that are in the shadow if (endingIndex > startingIndex) ShadowVertexCount = endingIndex - startingIndex + 1; @@ -663,7 +707,7 @@ namespace Barotrauma.Lights public void Remove() { - var chList = HullLists.Find(x => x.Submarine == ParentEntity.Submarine); + var chList = HullLists.Find(h => h.Submarine == ParentEntity.Submarine); if (chList != null) { @@ -672,8 +716,19 @@ namespace Barotrauma.Lights { HullLists.Remove(chList); } + foreach (ConvexHull ch2 in overlappingHulls) + { + for (int i = 0; i < 4; i++) + { + ch2.ignoreEdge[i] = false; + } + ch2.overlappingHulls.Remove(this); + foreach (ConvexHull ch in chList.List) + { + ch.MergeOverlappingSegments(ch2); + } + } } } } - } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index 803b28f82..dd25fa123 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -11,18 +11,6 @@ namespace Barotrauma.Lights { class LightManager { - private const float AmbientLightUpdateInterval = 0.2f; - private const float AmbientLightFalloff = 0.8f; - - /// - /// Enables a feature that makes lights inside the hull increase the brightness of the entire hull - /// and adjacent ones to some extent, if there are gaps for the lights to pass through. - /// Prevents unnaturally dark looking shadows in otherwise well-lit submarines, but disabled at least for - /// the time being because it makes the lighting behave unpredictably and may cause rooms to appear - /// excessively bright if different lighting conditions aren't tested and accounted for. - /// - private static readonly bool UseHullSpecificAmbientLight = false; - public static Entity ViewTarget { get; set; } private float currLightMapScale; @@ -57,7 +45,7 @@ namespace Barotrauma.Lights public Effect LosEffect { get; private set; } public Effect SolidColorEffect { get; private set; } - private List lights; + private readonly List lights; public bool LosEnabled = true; public LosMode LosMode = LosMode.Transparent; @@ -66,13 +54,8 @@ namespace Barotrauma.Lights public bool ObstructVision; - private Texture2D visionCircle; + private readonly Texture2D visionCircle; - private Dictionary hullAmbientLights; - private Dictionary smoothedHullAmbientLights; - - private float ambientLightUpdateTimer; - public IEnumerable Lights { get { return lights; } @@ -80,7 +63,7 @@ namespace Barotrauma.Lights public LightManager(GraphicsDevice graphics, ContentManager content) { - lights = new List(); + lights = new List(100); AmbientLight = new Color(20, 20, 20, 255); @@ -114,9 +97,6 @@ namespace Barotrauma.Lights }; } }); - - hullAmbientLights = new Dictionary(); - smoothedHullAmbientLights = new Dictionary(); } private void CreateRenderTargets(GraphicsDevice graphics) @@ -167,43 +147,12 @@ namespace Barotrauma.Lights } } - public void Update(float deltaTime) - { - if (UseHullSpecificAmbientLight) - { - if (ambientLightUpdateTimer > 0.0f) - { - ambientLightUpdateTimer -= deltaTime; - } - else - { - CalculateAmbientLights(); - ambientLightUpdateTimer = AmbientLightUpdateInterval; - } - - foreach (Hull hull in hullAmbientLights.Keys) - { - if (!smoothedHullAmbientLights.ContainsKey(hull)) - { - smoothedHullAmbientLights.Add(hull, Color.TransparentBlack); - } - } - - foreach (Hull hull in smoothedHullAmbientLights.Keys.ToList()) - { - Color targetColor = Color.TransparentBlack; - hullAmbientLights.TryGetValue(hull, out targetColor); - smoothedHullAmbientLights[hull] = Color.Lerp(smoothedHullAmbientLights[hull], targetColor, deltaTime); - } - } - } - - private List activeLights = new List(capacity: 100); + private readonly List activeLights = new List(capacity: 100); public void UpdateLightMap(GraphicsDevice graphics, SpriteBatch spriteBatch, Camera cam, RenderTarget2D backgroundObstructor = null) { - if (!LightingEnabled) return; - + if (!LightingEnabled) { return; } + if (Math.Abs(currLightMapScale - GameMain.Config.LightMapScale) > 0.01f) { //lightmap scale has changed -> recreate render targets @@ -229,7 +178,16 @@ namespace Barotrauma.Lights light.Position = light.ParentBody.DrawPosition; if (light.ParentSub != null) { light.Position -= light.ParentSub.DrawPosition; } } - if (!MathUtils.CircleIntersectsRectangle(light.WorldPosition, light.LightSourceParams.TextureRange, viewRect)) { continue; } + + float range = light.LightSourceParams.TextureRange; + if (light.LightSprite != null) + { + float spriteRange = Math.Max( + light.LightSprite.size.X * light.SpriteScale.X * (0.5f + Math.Abs(light.LightSprite.RelativeOrigin.X - 0.5f)), + light.LightSprite.size.Y * light.SpriteScale.Y * (0.5f + Math.Abs(light.LightSprite.RelativeOrigin.Y - 0.5f))); + range = Math.Max(spriteRange, range); + } + if (!MathUtils.CircleIntersectsRectangle(light.WorldPosition, range, viewRect)) { continue; } activeLights.Add(light); } @@ -252,16 +210,14 @@ namespace Barotrauma.Lights //draw background lights //--------------------------------------------------------------------------------------------------- graphics.SetRenderTarget(LightMap); - graphics.Clear(Color.Black); + graphics.Clear(AmbientLight); graphics.BlendState = BlendState.Additive; - bool backgroundSpritesDrawn = false; spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive, transformMatrix: spriteBatchTransform); foreach (LightSource light in activeLights) { if (!light.IsBackground) { continue; } light.DrawSprite(spriteBatch, cam); if (light.Color.A > 0 && light.Range > 0.0f) { light.DrawLightVolume(spriteBatch, lightEffect, transform); } - backgroundSpritesDrawn = true; } GameMain.ParticleManager.Draw(spriteBatch, true, null, Particles.ParticleBlendState.Additive); spriteBatch.End(); @@ -269,33 +225,34 @@ namespace Barotrauma.Lights //draw a black rectangle on hulls to hide background lights behind subs //--------------------------------------------------------------------------------------------------- - Dictionary visibleHulls = null; - if (backgroundSpritesDrawn) + if (backgroundObstructor != null) { - if (backgroundObstructor != null) - { - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied); - spriteBatch.Draw(backgroundObstructor, new Rectangle(0, 0, - (int)(GameMain.GraphicsWidth * currLightMapScale), (int)(GameMain.GraphicsHeight * currLightMapScale)), Color.Black); - spriteBatch.End(); - } - - visibleHulls = GetVisibleHulls(cam); - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Opaque, transformMatrix: spriteBatchTransform); - foreach (Rectangle drawRect in visibleHulls.Values) - { - //TODO: draw some sort of smoothed rectangle - GUI.DrawRectangle(spriteBatch, - new Vector2(drawRect.X, -drawRect.Y), - new Vector2(drawRect.Width, drawRect.Height), - Color.Black, true); - } + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied); + spriteBatch.Draw(backgroundObstructor, new Rectangle(0, 0, + (int)(GameMain.GraphicsWidth * currLightMapScale), (int)(GameMain.GraphicsHeight * currLightMapScale)), Color.Black); spriteBatch.End(); - - - graphics.BlendState = BlendState.Additive; } + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Opaque, transformMatrix: spriteBatchTransform); + Dictionary visibleHulls = GetVisibleHulls(cam); + foreach (KeyValuePair hull in visibleHulls) + { + GUI.DrawRectangle(spriteBatch, + new Vector2(hull.Value.X, -hull.Value.Y), + new Vector2(hull.Value.Width, hull.Value.Height), + hull.Key.AmbientLight == Color.TransparentBlack ? Color.Black : hull.Key.AmbientLight.Multiply(hull.Key.AmbientLight.A / 255.0f), true); + } + spriteBatch.End(); + + SolidColorEffect.CurrentTechnique = SolidColorEffect.Techniques["SolidColor"]; + SolidColorEffect.Parameters["color"].SetValue(AmbientLight.ToVector4()); + spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, transformMatrix: spriteBatchTransform, effect: SolidColorEffect); + Submarine.DrawDamageable(spriteBatch, null); + spriteBatch.End(); + + graphics.BlendState = BlendState.Additive; + + //draw the focused item and character to highlight them, //and light sprites (done before drawing the actual light volumes so we can make characters obstruct the highlights and sprites) //--------------------------------------------------------------------------------------------------- @@ -318,33 +275,37 @@ namespace Barotrauma.Lights //draw characters to obstruct the highlighted items/characters and light sprites //--------------------------------------------------------------------------------------------------- - SolidColorEffect.CurrentTechnique = SolidColorEffect.Techniques["SolidColor"]; - SolidColorEffect.Parameters["color"].SetValue(Color.Black.ToVector4()); + SolidColorEffect.CurrentTechnique = SolidColorEffect.Techniques["SolidVertexColor"]; spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, 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.CurrentHull == null || !character.Enabled) { continue; } + if (Character.Controlled?.FocusedCharacter == character) { continue; } + Color lightColor = character.CurrentHull.AmbientLight == Color.TransparentBlack ? + Color.Black : + character.CurrentHull.AmbientLight.Multiply(character.CurrentHull.AmbientLight.A / 255.0f).Opaque(); foreach (Limb limb in character.AnimController.Limbs) { - if (limb.DeformSprite != null) continue; - limb.Draw(spriteBatch, cam, Color.Black); + if (limb.DeformSprite != null) { continue; } + limb.Draw(spriteBatch, cam, lightColor); } } spriteBatch.End(); - DeformableSprite.Effect.CurrentTechnique = DeformableSprite.Effect.Techniques["DeformShaderSolidColor"]; - DeformableSprite.Effect.Parameters["solidColor"].SetValue(Color.Black.ToVector4()); + DeformableSprite.Effect.CurrentTechnique = DeformableSprite.Effect.Techniques["DeformShaderSolidVertexColor"]; DeformableSprite.Effect.CurrentTechnique.Passes[0].Apply(); spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, transformMatrix: spriteBatchTransform); foreach (Character character in Character.CharacterList) { - if (character.CurrentHull == null || !character.Enabled) continue; - if (Character.Controlled?.FocusedCharacter == character) continue; + if (character.CurrentHull == null || !character.Enabled) { continue; } + if (Character.Controlled?.FocusedCharacter == character) { continue; } + Color lightColor = character.CurrentHull.AmbientLight == Color.TransparentBlack ? + Color.Black : + character.CurrentHull.AmbientLight.Multiply(character.CurrentHull.AmbientLight.A / 255.0f).Opaque(); foreach (Limb limb in character.AnimController.Limbs) { - if (limb.DeformSprite == null) continue; - limb.Draw(spriteBatch, cam, Color.Black); + if (limb.DeformSprite == null) { continue; } + limb.Draw(spriteBatch, cam, lightColor); } } spriteBatch.End(); @@ -355,8 +316,6 @@ namespace Barotrauma.Lights //--------------------------------------------------------------------------------------------------- spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive, transformMatrix: spriteBatchTransform); - GUI.DrawRectangle(spriteBatch, new Rectangle(cam.WorldView.X, -cam.WorldView.Y, cam.WorldView.Width, cam.WorldView.Height), AmbientLight, isFilled: true); - spriteBatch.Draw(LimbLightMap, new Rectangle(cam.WorldView.X, -cam.WorldView.Y, cam.WorldView.Width, cam.WorldView.Height), Color.White); foreach (ElectricalDischarger discharger in ElectricalDischarger.List) @@ -373,24 +332,7 @@ namespace Barotrauma.Lights lightEffect.World = transform; GameMain.ParticleManager.Draw(spriteBatch, false, null, Particles.ParticleBlendState.Additive); - - if (UseHullSpecificAmbientLight) - { - if (visibleHulls == null) - { - visibleHulls = GetVisibleHulls(cam); - } - foreach (Hull hull in smoothedHullAmbientLights.Keys) - { - if (smoothedHullAmbientLights[hull].A < 0.01f) continue; - if (!visibleHulls.TryGetValue(hull, out Rectangle drawRect)) continue; - GUI.DrawRectangle(spriteBatch, - new Vector2(drawRect.X, -drawRect.Y), - new Vector2(hull.Rect.Width, hull.Rect.Height), - smoothedHullAmbientLights[hull], true); - } - } - + if (Character.Controlled != null) { Vector2 haloDrawPos = Character.Controlled.DrawPosition; @@ -592,7 +534,10 @@ namespace Barotrauma.Lights } } - penumbraVerts.AddRange(convexHull.PenumbraVertices); + if (convexHull.ShadowVertexCount > 0) + { + penumbraVerts.AddRange(convexHull.PenumbraVertices); + } } if (shadowVerts.Count > 0) @@ -613,86 +558,6 @@ namespace Barotrauma.Lights graphics.SetRenderTarget(null); } - - private void CalculateAmbientLights() - { - hullAmbientLights.Clear(); - - foreach (LightSource light in lights) - { - if (light.Color.A < 1f || light.Range < 1.0f || light.IsBackground) continue; - - var newAmbientLights = AmbientLightHulls(light); - foreach (Hull hull in newAmbientLights.Keys) - { - if (hullAmbientLights.ContainsKey(hull)) - { - //hull already lit by some other light source -> add the ambient lights up - hullAmbientLights[hull] = new Color( - hullAmbientLights[hull].R + newAmbientLights[hull].R, - hullAmbientLights[hull].G + newAmbientLights[hull].G, - hullAmbientLights[hull].B + newAmbientLights[hull].B, - hullAmbientLights[hull].A + newAmbientLights[hull].A); - } - else - { - hullAmbientLights.Add(hull, newAmbientLights[hull]); - } - } - } - } - - /// - /// Add ambient light to the hull the lightsource is inside + all adjacent hulls connected by a gap - /// - private Dictionary AmbientLightHulls(LightSource light) - { - Dictionary hullAmbientLight = new Dictionary(); - - var hull = Hull.FindHull(light.WorldPosition); - if (hull == null) return hullAmbientLight; - - return AmbientLightHulls(hull, hullAmbientLight, light.Color * Math.Min(light.Range / 1000.0f, 1.0f)); - } - - /// - /// A flood fill algorithm that adds ambient light to all hulls the starting hull is connected to - /// - private Dictionary AmbientLightHulls(Hull hull, Dictionary hullAmbientLight, Color currColor) - { - if (hullAmbientLight.ContainsKey(hull)) - { - if (hullAmbientLight[hull].A > currColor.A) - return hullAmbientLight; - else - hullAmbientLight[hull] = currColor; - } - else - { - hullAmbientLight.Add(hull, currColor); - } - - Color nextHullLight = currColor * AmbientLightFalloff; - //light getting too dark to notice -> no need to spread further - if (nextHullLight.A < 20) return hullAmbientLight; - - //use hashset to make sure that each hull is only included once - HashSet hulls = new HashSet(); - foreach (Gap g in hull.ConnectedGaps) - { - if (!g.IsRoomToRoom || !g.PassAmbientLight || g.Open < 0.5f) continue; - - hulls.Add((g.linkedTo[0] == hull ? g.linkedTo[1] : g.linkedTo[0]) as Hull); - } - - foreach (Hull h in hulls) - { - hullAmbientLight = AmbientLightHulls(h, hullAmbientLight, nextHullLight); - } - - return hullAmbientLight; - } - public void ClearLights() { lights.Clear(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index 8a9114105..76bcc63ae 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -16,7 +16,7 @@ namespace Barotrauma.Lights public Dictionary SerializableProperties { get; private set; } = new Dictionary(); - [Serialize("1.0,1.0,1.0,1.0", true), Editable] + [Serialize("1.0,1.0,1.0,1.0", true, alwaysUseInstanceValues: true), Editable] public Color Color { get; @@ -25,7 +25,7 @@ namespace Barotrauma.Lights private float range; - [Serialize(100.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2048.0f)] + [Serialize(100.0f, true, alwaysUseInstanceValues: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2048.0f)] public float Range { get { return range; } @@ -331,7 +331,7 @@ namespace Barotrauma.Lights if (lightSourceParams.DeformableLightSpriteElement != null) { - DeformableLightSprite = new DeformableSprite(lightSourceParams.DeformableLightSpriteElement); + DeformableLightSprite = new DeformableSprite(lightSourceParams.DeformableLightSpriteElement, invert: true); } } @@ -342,7 +342,7 @@ namespace Barotrauma.Lights lightSourceParams.Persistent = true; if (lightSourceParams.DeformableLightSpriteElement != null) { - DeformableLightSprite = new DeformableSprite(lightSourceParams.DeformableLightSpriteElement); + DeformableLightSprite = new DeformableSprite(lightSourceParams.DeformableLightSpriteElement, invert: true); } } @@ -952,23 +952,44 @@ namespace Barotrauma.Lights { Vector2 origin = DeformableLightSprite.Origin; Vector2 drawPos = position; - if (ParentSub != null) drawPos += ParentSub.DrawPosition; + if (ParentSub != null) + { + drawPos += ParentSub.DrawPosition; + } + + if (LightSpriteEffect == SpriteEffects.FlipHorizontally) + { + origin.X = DeformableLightSprite.Sprite.SourceRect.Width - origin.X; + } + if (LightSpriteEffect == SpriteEffects.FlipVertically) + { + origin.Y = DeformableLightSprite.Sprite.SourceRect.Height - origin.Y; + } DeformableLightSprite.Draw( cam, new Vector3(drawPos, 0.0f), origin, -Rotation, SpriteScale, new Color(Color, lightSourceParams.OverrideLightSpriteAlpha ?? Color.A / 255.0f), - LightSpriteEffect == SpriteEffects.FlipHorizontally); + LightSpriteEffect == SpriteEffects.FlipVertically); } if (LightSprite != null) { Vector2 origin = LightSprite.Origin; - if (LightSpriteEffect == SpriteEffects.FlipHorizontally) origin.X = LightSprite.SourceRect.Width - origin.X; - if (LightSpriteEffect == SpriteEffects.FlipVertically) origin.Y = LightSprite.SourceRect.Height - origin.Y; + if (LightSpriteEffect == SpriteEffects.FlipHorizontally) + { + origin.X = LightSprite.SourceRect.Width - origin.X; + } + if (LightSpriteEffect == SpriteEffects.FlipVertically) + { + origin.Y = LightSprite.SourceRect.Height - origin.Y; + } Vector2 drawPos = position; - if (ParentSub != null) drawPos += ParentSub.DrawPosition; + if (ParentSub != null) + { + drawPos += ParentSub.DrawPosition; + } drawPos.Y = -drawPos.Y; LightSprite.Draw( diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs index 22c2b7627..d3e649c98 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs @@ -2,7 +2,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; @@ -13,11 +13,28 @@ namespace Barotrauma public override void Draw(SpriteBatch spriteBatch, bool editing, bool back = true) { if (!editing || wallVertices == null) { return; } + + Draw(spriteBatch, Position); - Color color = IsHighlighted ? GUI.Style.Orange : GUI.Style.Green; + if (!Item.ShowLinks) { return; } + + foreach (MapEntity e in linkedTo) + { + bool isLinkAllowed = e is Item item && item.HasTag("dock"); + + GUI.DrawLine(spriteBatch, + new Vector2(WorldPosition.X, -WorldPosition.Y), + new Vector2(e.WorldPosition.X, -e.WorldPosition.Y), + isLinkAllowed ? GUI.Style.Green * 0.5f : GUI.Style.Red * 0.5f, width: 3); + } + } + + public void Draw(SpriteBatch spriteBatch, Vector2 drawPos, float alpha = 1.0f) + { + Color color = (IsHighlighted) ? GUI.Style.Orange : GUI.Style.Green; if (IsSelected) { color = GUI.Style.Red; } - Vector2 pos = Position; + Vector2 pos = drawPos; for (int i = 0; i < wallVertices.Count; i++) { @@ -28,26 +45,14 @@ namespace Barotrauma endPos.Y = -endPos.Y; GUI.DrawLine(spriteBatch, - startPos, - endPos, - color, 0.0f, 5); + startPos, + endPos, + color * alpha, 0.0f, 5); } pos.Y = -pos.Y; - GUI.DrawLine(spriteBatch, pos + Vector2.UnitY * 50.0f, pos - Vector2.UnitY * 50.0f, color, 0.0f, 5); - GUI.DrawLine(spriteBatch, pos + Vector2.UnitX * 50.0f, pos - Vector2.UnitX * 50.0f, color, 0.0f, 5); - - if (!Item.ShowLinks) { return; } - - foreach (MapEntity e in linkedTo) - { - bool isLinkAllowed = e is Item item && item.HasTag("dock"); - - GUI.DrawLine(spriteBatch, - new Vector2(WorldPosition.X, -WorldPosition.Y), - new Vector2(e.WorldPosition.X, -e.WorldPosition.Y), - isLinkAllowed ? GUI.Style.Green * 0.5f : GUI.Style.Red * 0.5f, width: 3); - } + GUI.DrawLine(spriteBatch, pos + Vector2.UnitY * 50.0f, pos - Vector2.UnitY * 50.0f, color * alpha, 0.0f, 5); + GUI.DrawLine(spriteBatch, pos + Vector2.UnitX * 50.0f, pos - Vector2.UnitX * 50.0f, color * alpha, 0.0f, 5); } public override void UpdateEditing(Camera cam) @@ -100,10 +105,9 @@ namespace Barotrauma } var pathContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), isHorizontal: true); - var pathBox = new GUITextBox(new RectTransform(new Vector2(0.75f, 1.0f), pathContainer.RectTransform), filePath, font: GUI.SmallFont); var reloadButton = new GUIButton(new RectTransform(new Vector2(0.25f / pathBox.RectTransform.RelativeSize.X, 1.0f), pathBox.RectTransform, Anchor.CenterRight, Pivot.CenterLeft), - TextManager.Get("ReloadLinkedSub"), style: "GUIButtonSmall") + TextManager.Get("ReloadLinkedSub"), style: "GUIButtonSmall") { OnClicked = Reload, UserData = pathBox, @@ -133,6 +137,7 @@ namespace Barotrauma XDocument doc = SubmarineInfo.OpenFile(pathBox.Text); if (doc == null || doc.Root == null) return false; + doc.Root.SetAttributeValue("filepath", pathBox.Text); pathBox.Flash(GUI.Style.Green); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index a71ea1459..5bb384020 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -16,6 +16,8 @@ namespace Barotrauma private static Vector2 startMovingPos = Vector2.Zero; + private static float keyDelay; + public static Vector2 StartMovingPos => startMovingPos; // Quick undo/redo for size and movement only. TODO: Remove if we do a more general implementation. @@ -132,7 +134,7 @@ namespace Barotrauma if (highlightedListBox == null || (GUI.MouseOn != highlightedListBox && !highlightedListBox.IsParentOf(GUI.MouseOn))) { - UpdateHighlightedListBox(null); + UpdateHighlightedListBox(null, false); return; } } @@ -147,11 +149,15 @@ namespace Barotrauma { if (PlayerInput.KeyDown(Keys.Delete)) { - selectedList.ForEach(e => e.Remove()); + selectedList.ForEach(e => + { + //orphaned wires may already have been removed + if (!e.Removed) { e.Remove(); } + }); selectedList.Clear(); } - if (PlayerInput.KeyDown(Keys.LeftControl) || PlayerInput.KeyDown(Keys.RightControl)) + if (PlayerInput.IsCtrlDown()) { if (PlayerInput.KeyHit(Keys.C)) { @@ -244,32 +250,7 @@ namespace Barotrauma } } - if (PlayerInput.MouseSpeed.LengthSquared() > 10) - { - highlightTimer = 0.0f; - } - else - { - bool mouseNearHighlightBox = false; - - if (highlightedListBox != null) - { - Rectangle expandedRect = highlightedListBox.Rect; - expandedRect.Inflate(20, 20); - mouseNearHighlightBox = expandedRect.Contains(PlayerInput.MousePosition); - if (!mouseNearHighlightBox) highlightedListBox = null; - } - - highlightTimer += (float)Timing.Step; - if (highlightTimer > 1.0f) - { - if (!mouseNearHighlightBox) - { - UpdateHighlightedListBox(highlightedEntities); - highlightTimer = 0.0f; - } - } - } + UpdateHighlighting(highlightedEntities); } if (highLightedEntity != null) highLightedEntity.isHighlighted = true; @@ -277,35 +258,65 @@ namespace Barotrauma if (GUI.KeyboardDispatcher.Subscriber == null) { + int up = PlayerInput.KeyDown(Keys.Up) ? 1 : 0, + down = PlayerInput.KeyDown(Keys.Down) ? -1 : 0, + left = PlayerInput.KeyDown(Keys.Left) ? -1 : 0, + right = PlayerInput.KeyDown(Keys.Right) ? 1 : 0; + + int xKeysDown = (left + right); + int yKeysDown = (up + down); + + if (xKeysDown != 0 || yKeysDown != 0) { keyDelay += (float) Timing.Step; } else { keyDelay = 0; } + Vector2 nudgeAmount = Vector2.Zero; - if (PlayerInput.KeyHit(Keys.Up)) nudgeAmount.Y = 1f; + + if (keyDelay >= 0.5f) + { + nudgeAmount.Y = yKeysDown; + nudgeAmount.X = xKeysDown; + } + + if (PlayerInput.KeyHit(Keys.Up)) nudgeAmount.Y = 1f; if (PlayerInput.KeyHit(Keys.Down)) nudgeAmount.Y = -1f; if (PlayerInput.KeyHit(Keys.Left)) nudgeAmount.X = -1f; - if (PlayerInput.KeyHit(Keys.Right)) nudgeAmount.X = 1f; + if (PlayerInput.KeyHit(Keys.Right)) nudgeAmount.X = 1f; if (nudgeAmount != Vector2.Zero) { - foreach (MapEntity entityToNudge in selectedList) - { - entityToNudge.Move(nudgeAmount); - } + foreach (MapEntity entityToNudge in selectedList) { entityToNudge.Move(nudgeAmount); } } } + else + { + keyDelay = 0; + } + + bool isShiftDown = PlayerInput.IsShiftDown(); //started moving selected entities if (startMovingPos != Vector2.Zero) { + Item targetContainer = GetPotentialContainer(position, selectedList); + + if (targetContainer != null) { targetContainer.IsHighlighted = true; } + if (PlayerInput.PrimaryMouseButtonReleased()) { //mouse released -> move the entities to the new position of the mouse Vector2 moveAmount = position - startMovingPos; - moveAmount.X = (float)(moveAmount.X > 0.0f ? Math.Floor(moveAmount.X / Submarine.GridSize.X) : Math.Ceiling(moveAmount.X / Submarine.GridSize.X)) * Submarine.GridSize.X; - moveAmount.Y = (float)(moveAmount.Y > 0.0f ? Math.Floor(moveAmount.Y / Submarine.GridSize.Y) : Math.Ceiling(moveAmount.Y / Submarine.GridSize.Y)) * Submarine.GridSize.Y; - if (Math.Abs(moveAmount.X) >= Submarine.GridSize.X || Math.Abs(moveAmount.Y) >= Submarine.GridSize.Y) + + if (!isShiftDown) { - moveAmount = Submarine.VectorToWorldGrid(moveAmount); + moveAmount.X = (float)(moveAmount.X > 0.0f ? Math.Floor(moveAmount.X / Submarine.GridSize.X) : Math.Ceiling(moveAmount.X / Submarine.GridSize.X)) * Submarine.GridSize.X; + moveAmount.Y = (float)(moveAmount.Y > 0.0f ? Math.Floor(moveAmount.Y / Submarine.GridSize.Y) : Math.Ceiling(moveAmount.Y / Submarine.GridSize.Y)) * Submarine.GridSize.Y; + } + + if (Math.Abs(moveAmount.X) >= Submarine.GridSize.X || Math.Abs(moveAmount.Y) >= Submarine.GridSize.Y || isShiftDown) + { + if (!isShiftDown) { moveAmount = Submarine.VectorToWorldGrid(moveAmount); } + //clone - if (PlayerInput.KeyDown(Keys.LeftControl) || PlayerInput.KeyDown(Keys.RightControl)) + if (PlayerInput.IsCtrlDown()) { var clones = Clone(selectedList); selectedList = clones; @@ -313,6 +324,7 @@ namespace Barotrauma } else // move { + List deposited = new List(); foreach (MapEntity e in selectedList) { if (e.rectMemento == null) @@ -321,8 +333,23 @@ namespace Barotrauma e.rectMemento.Store(e.Rect); } e.Move(moveAmount); + + if (isShiftDown && e is Item item && targetContainer != null) + { + if (targetContainer.OwnInventory.TryPutItem(item, Character.Controlled)) + { + GUI.PlayUISound(GUISoundType.DropItem); + deposited.Add(item); + } + else + { + GUI.PlayUISound(GUISoundType.PickItemFail); + } + } e.rectMemento.Store(e.Rect); } + + deposited.ForEach(entity => { selectedList.Remove(entity); }); } } startMovingPos = Vector2.Zero; @@ -357,8 +384,7 @@ namespace Barotrauma if (PlayerInput.PrimaryMouseButtonReleased()) { - if (PlayerInput.KeyDown(Keys.LeftControl) || - PlayerInput.KeyDown(Keys.RightControl)) + if (PlayerInput.IsCtrlDown()) { foreach (MapEntity e in newSelection) { @@ -441,7 +467,84 @@ namespace Barotrauma } } - private static void UpdateHighlightedListBox(List highlightedEntities) + public static Item GetPotentialContainer(Vector2 position, List entities = null) + { + Item targetContainer = null; + bool isShiftDown = PlayerInput.IsShiftDown(); + + if (!isShiftDown) return null; + + foreach (MapEntity e in mapEntityList) + { + if (!e.SelectableInEditor ||!(e is Item potentialContainer)) { continue; } + + if (e.IsMouseOn(position)) + { + if (entities == null) + { + if (potentialContainer.OwnInventory != null && potentialContainer.ParentInventory == null && !potentialContainer.OwnInventory.IsFull()) + { + targetContainer = potentialContainer; + break; + } + } + else + { + foreach (MapEntity selectedEntity in entities) + { + if (!(selectedEntity is Item selectedItem)) { continue; } + if (potentialContainer.OwnInventory != null && potentialContainer.ParentInventory == null && potentialContainer != selectedItem && + potentialContainer.OwnInventory.CanBePut(selectedItem)) + { + targetContainer = potentialContainer; + break; + } + } + } + } + if (targetContainer != null) { break; } + } + + return targetContainer; + } + + /// + /// Updates the logic that runs the highlight box when the mouse is sitting still. + /// + /// + /// + /// true to give items tooltip showing their connection + public static void UpdateHighlighting(List highlightedEntities, bool wiringMode = false) + { + if (PlayerInput.MouseSpeed.LengthSquared() > 10) + { + highlightTimer = 0.0f; + } + else + { + bool mouseNearHighlightBox = false; + + if (highlightedListBox != null) + { + Rectangle expandedRect = highlightedListBox.Rect; + expandedRect.Inflate(20, 20); + mouseNearHighlightBox = expandedRect.Contains(PlayerInput.MousePosition); + if (!mouseNearHighlightBox) highlightedListBox = null; + } + + highlightTimer += (float)Timing.Step; + if (highlightTimer > 1.0f) + { + if (!mouseNearHighlightBox) + { + UpdateHighlightedListBox(highlightedEntities, wiringMode); + highlightTimer = 0.0f; + } + } + } + } + + private static void UpdateHighlightedListBox(List highlightedEntities, bool wiringMode) { if (highlightedEntities == null || highlightedEntities.Count < 2) { @@ -458,14 +561,37 @@ namespace Barotrauma highlightedListBox = new GUIListBox(new RectTransform(new Point(180, highlightedEntities.Count * 18 + 5), GUI.Canvas) { + MaxSize = new Point(int.MaxValue, 256), ScreenSpaceOffset = PlayerInput.MousePosition.ToPoint() + new Point(15) }, style: "GUIToolTip"); foreach (MapEntity entity in highlightedEntities) { - var textBlock = new GUITextBlock(new RectTransform(new Point(highlightedListBox.Content.Rect.Width, 15), highlightedListBox.Content.RectTransform), - ToolBox.LimitString(entity.Name, GUI.SmallFont, 140), font: GUI.SmallFont) + var tooltip = string.Empty; + + if (wiringMode && entity is Item item) { + var wire = item.GetComponent(); + if (wire?.Connections != null) + { + for (var i = 0; i < wire.Connections.Length; i++) + { + var conn = wire.Connections[i]; + if (conn != null) + { + string[] tags = { "[item]", "[pin]" }; + string[] values = { conn.Item?.Name, conn.Name }; + tooltip += TextManager.GetWithVariables("wirelistformat",tags , values); + } + if (i != wire.Connections.Length - 1) { tooltip += '\n'; } + } + } + } + + var textBlock = new GUITextBlock(new RectTransform(new Point(highlightedListBox.Content.Rect.Width, 15), highlightedListBox.Content.RectTransform), + ToolBox.LimitString(entity.Name, GUI.SmallFont, 140), font: GUI.SmallFont) + { + ToolTip = tooltip, UserData = entity }; } @@ -474,8 +600,7 @@ namespace Barotrauma { MapEntity entity = obj as MapEntity; - if (PlayerInput.KeyDown(Keys.LeftControl) || - PlayerInput.KeyDown(Keys.RightControl)) + if (PlayerInput.IsCtrlDown() && !wiringMode) { if (selectedList.Contains(entity)) { @@ -485,11 +610,10 @@ namespace Barotrauma { AddSelection(entity); } + + return true; } - else - { - SelectEntity(entity); - } + SelectEntity(entity); return true; }; @@ -558,6 +682,10 @@ namespace Barotrauma { item.UpdateSpriteStates(deltaTime); } + else if (me is Structure structure) + { + structure.UpdateSpriteStates(deltaTime); + } } } @@ -575,32 +703,52 @@ namespace Barotrauma { Vector2 moveAmount = position - startMovingPos; moveAmount.Y = -moveAmount.Y; - moveAmount.X = (float)(moveAmount.X > 0.0f ? Math.Floor(moveAmount.X / Submarine.GridSize.X) : Math.Ceiling(moveAmount.X / Submarine.GridSize.X)) * Submarine.GridSize.X; - moveAmount.Y = (float)(moveAmount.Y > 0.0f ? Math.Floor(moveAmount.Y / Submarine.GridSize.Y) : Math.Ceiling(moveAmount.Y / Submarine.GridSize.Y)) * Submarine.GridSize.Y; + + bool isShiftDown = PlayerInput.IsShiftDown(); + + if (!isShiftDown) + { + moveAmount.X = (float)(moveAmount.X > 0.0f ? Math.Floor(moveAmount.X / Submarine.GridSize.X) : Math.Ceiling(moveAmount.X / Submarine.GridSize.X)) * Submarine.GridSize.X; + moveAmount.Y = (float)(moveAmount.Y > 0.0f ? Math.Floor(moveAmount.Y / Submarine.GridSize.Y) : Math.Ceiling(moveAmount.Y / Submarine.GridSize.Y)) * Submarine.GridSize.Y; + } //started moving the selected entities - if (Math.Abs(moveAmount.X) >= Submarine.GridSize.X || Math.Abs(moveAmount.Y) >= Submarine.GridSize.Y) + if (Math.Abs(moveAmount.X) >= Submarine.GridSize.X || Math.Abs(moveAmount.Y) >= Submarine.GridSize.Y || isShiftDown) { foreach (MapEntity e in selectedList) { SpriteEffects spriteEffects = SpriteEffects.None; - if (e is Item item) + switch (e) { - if (item.FlippedX && item.Prefab.CanSpriteFlipX) spriteEffects ^= SpriteEffects.FlipHorizontally; - if (item.flippedY && item.Prefab.CanSpriteFlipY) spriteEffects ^= SpriteEffects.FlipVertically; - } - else if (e is Structure structure) - { - if (structure.FlippedX && structure.Prefab.CanSpriteFlipX) spriteEffects ^= SpriteEffects.FlipHorizontally; - if (structure.flippedY && structure.Prefab.CanSpriteFlipY) spriteEffects ^= SpriteEffects.FlipVertically; - } - else if (e is WayPoint wayPoint) - { - Vector2 drawPos = e.WorldPosition; - drawPos.Y = -drawPos.Y; - drawPos += moveAmount; - wayPoint.Draw(spriteBatch, drawPos); - continue; + case Item item: + { + if (item.FlippedX && item.Prefab.CanSpriteFlipX) spriteEffects ^= SpriteEffects.FlipHorizontally; + if (item.flippedY && item.Prefab.CanSpriteFlipY) spriteEffects ^= SpriteEffects.FlipVertically; + break; + } + case Structure structure: + { + if (structure.FlippedX && structure.Prefab.CanSpriteFlipX) spriteEffects ^= SpriteEffects.FlipHorizontally; + if (structure.flippedY && structure.Prefab.CanSpriteFlipY) spriteEffects ^= SpriteEffects.FlipVertically; + break; + } + case WayPoint wayPoint: + { + Vector2 drawPos = e.WorldPosition; + drawPos.Y = -drawPos.Y; + drawPos += moveAmount; + wayPoint.Draw(spriteBatch, drawPos); + continue; + } + case LinkedSubmarine linkedSub: + { + var ma = moveAmount; + ma.Y = -ma.Y; + Vector2 lPos = linkedSub.Position; + lPos += ma; + linkedSub.Draw(spriteBatch, lPos, alpha: 0.5f); + break; + } } e.prefab?.DrawPlacing(spriteBatch, new Rectangle(e.WorldRect.Location + new Point((int)moveAmount.X, (int)-moveAmount.Y), e.WorldRect.Size), e.Scale, spriteEffects); @@ -656,7 +804,7 @@ namespace Barotrauma } } - if ((PlayerInput.KeyDown(Keys.LeftControl) || PlayerInput.KeyDown(Keys.RightControl))) + if (PlayerInput.IsCtrlDown()) { if (PlayerInput.KeyHit(Keys.N)) { @@ -734,7 +882,10 @@ namespace Barotrauma CopyEntities(entities); - entities.ForEach(e => e.Remove()); + entities.ForEach(e => + { + e.Remove(); + }); entities.Clear(); } @@ -847,6 +998,7 @@ namespace Barotrauma resizeDirX = x; resizeDirY = y; resizing = true; + startMovingPos = Vector2.Zero; } } } @@ -864,6 +1016,11 @@ namespace Barotrauma Vector2 mousePos = Submarine.MouseToWorldGrid(cam, Submarine.MainSub); + if (PlayerInput.IsShiftDown()) + { + mousePos = cam.ScreenToWorld(PlayerInput.MousePosition); + } + if (resizeDirX > 0) { mousePos.X = Math.Max(mousePos.X, rect.X + Submarine.GridSize.X); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs index 676a56d2d..640cd4a95 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs @@ -8,6 +8,12 @@ namespace Barotrauma { public virtual void UpdatePlacing(Camera cam) { + if (PlayerInput.SecondaryMouseButtonClicked()) + { + selected = null; + return; + } + Vector2 placeSize = Submarine.GridSize; if (placePosition == Vector2.Zero) @@ -41,12 +47,6 @@ namespace Barotrauma newRect.Y = -newRect.Y; } - - if (PlayerInput.SecondaryMouseButtonHeld()) - { - placePosition = Vector2.Zero; - selected = null; - } } public virtual void DrawPlacing(SpriteBatch spriteBatch, Camera cam) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index e60fb34c9..a8b74b8b5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -24,6 +24,10 @@ namespace Barotrauma { get { + if (!GameMain.SubEditorScreen.ShowThalamus && prefab.Category.HasFlag(MapEntityCategory.Thalamus)) + { + return false; + } return HasBody ? ShowWalls : ShowStructures; } } @@ -46,6 +50,8 @@ namespace Barotrauma decorativeSprite.Sprite.EnsureLazyLoaded(); spriteAnimState.Add(decorativeSprite, new DecorativeSprite.State()); } + + UpdateSpriteStates(0.0f); } partial void CreateConvexHull(Vector2 position, Vector2 size, float rotation) @@ -334,7 +340,7 @@ namespace Barotrauma float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState); Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState) * Scale; decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color, - rotation, Scale, prefab.sprite.effects, + rotation, decorativeSprite.Scale * Scale, prefab.sprite.effects, depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - prefab.sprite.Depth), 0.999f)); } prefab.sprite.effects = oldEffects; @@ -435,7 +441,7 @@ namespace Barotrauma byte sectionCount = msg.ReadByte(); if (sectionCount != Sections.Length) { - string errorMsg = $"Error while reading a network event for the structure \"{Name}\". Section count does not match (server: {sectionCount} client: {Sections.Length})"; + string errorMsg = $"Error while reading a network event for the structure \"{Name} ({ID})\". Section count does not match (server: {sectionCount} client: {Sections.Length})"; DebugConsole.NewMessage(errorMsg, Color.Red); GameAnalyticsManager.AddErrorEventOnce("Structure.ClientRead:SectionCountMismatch", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs index dec803786..52961c361 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs @@ -18,6 +18,12 @@ namespace Barotrauma public override void UpdatePlacing(Camera cam) { + if (PlayerInput.SecondaryMouseButtonClicked()) + { + selected = null; + return; + } + Vector2 position = Submarine.MouseToWorldGrid(cam, Submarine.MainSub); Vector2 size = ScaledSize; Rectangle newRect = new Rectangle((int)position.X, (int)position.Y, (int)size.X, (int)size.Y); @@ -59,8 +65,6 @@ namespace Barotrauma return; } } - - if (PlayerInput.SecondaryMouseButtonHeld()) selected = null; } public override void DrawPlacing(SpriteBatch spriteBatch, Camera cam) @@ -70,9 +74,6 @@ namespace Barotrauma if (placePosition == Vector2.Zero) { - if (PlayerInput.PrimaryMouseButtonHeld()) - placePosition = Submarine.MouseToWorldGrid(cam, Submarine.MainSub); - newRect.X = (int)position.X; newRect.Y = (int)position.Y; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index aa88465d9..43a5d1494 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -6,9 +6,10 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; +using Barotrauma.Items.Components; namespace Barotrauma { @@ -86,7 +87,7 @@ namespace Barotrauma existingSound = GameMain.SoundManager.LoadSound(filename, stream); if (existingSound == null) { return null; } } - catch (FileNotFoundException e) + catch (System.IO.FileNotFoundException e) { string errorMsg = "Failed to load sound file \"" + filename + "\"."; DebugConsole.ThrowError(errorMsg, e); @@ -223,7 +224,7 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, worldBorders, Color.White, false, 0, 5); - if (sub.subBody == null || sub.subBody.PositionBuffer.Count < 2) continue; + if (sub.SubBody == null || sub.subBody.PositionBuffer.Count < 2) continue; Vector2 prevPos = ConvertUnits.ToDisplayUnits(sub.subBody.PositionBuffer[0].Position); prevPos.Y = -prevPos.Y; @@ -246,7 +247,7 @@ namespace Barotrauma public static Color DamageEffectColor; private static readonly List depthSortedDamageable = new List(); - public static void DrawDamageable(SpriteBatch spriteBatch, Effect damageEffect, bool editing = false) + public static void DrawDamageable(SpriteBatch spriteBatch, Effect damageEffect, bool editing = false, Predicate predicate = null) { var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.mapEntityList; @@ -257,6 +258,10 @@ namespace Barotrauma { if (e is Structure structure && structure.DrawDamageEffect) { + if (predicate != null) + { + if (!predicate(e)) continue; + } float drawDepth = structure.GetDrawDepth(); int i = 0; while (i < depthSortedDamageable.Count) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs index b638ec40f..a168a3952 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs @@ -1,7 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Text; using System.Xml.Linq; @@ -19,9 +19,9 @@ namespace Barotrauma { try { - using (MemoryStream mem = new MemoryStream(Convert.FromBase64String(previewImageData))) + using (System.IO.MemoryStream mem = new System.IO.MemoryStream(Convert.FromBase64String(previewImageData))) { - var texture = TextureLoader.FromStream(mem, path: FilePath); + var texture = TextureLoader.FromStream(mem, path: FilePath, compress: false); if (texture == null) { throw new Exception("PreviewImage texture returned null"); } PreviewImage = new Sprite(texture, null, null); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs index b4bf3c3e8..8baa8eb61 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs @@ -1,6 +1,7 @@ using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; @@ -8,7 +9,7 @@ namespace Barotrauma { partial class WayPoint : MapEntity { - private static Dictionary iconSprites; + private static Dictionary iconSprites; private const int WaypointSize = 12, SpawnPointSize = 32; public override bool IsVisible(Rectangle worldView) @@ -55,10 +56,18 @@ namespace Barotrauma Color.White); } - Sprite sprite = iconSprites[SpawnType]; + Sprite sprite = iconSprites[SpawnType.ToString()]; if (spawnType == SpawnType.Human && AssignedJob?.Icon != null) { - sprite = iconSprites[SpawnType.Path]; + sprite = iconSprites["Path"]; + } + else if (ConnectedDoor != null) + { + sprite = iconSprites["Door"]; + } + else if (Ladders != null) + { + sprite = iconSprites["Ladder"]; } sprite.Draw(spriteBatch, drawPos, clr, scale: iconSize / (float)sprite.SourceRect.Width, depth: 0.001f); sprite.RelativeOrigin = Vector2.One * 0.5f; @@ -107,59 +116,72 @@ namespace Barotrauma { editingHUD = CreateEditingHUD(); } - + if (IsSelected && PlayerInput.PrimaryMouseButtonClicked()) { Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition); - // Update gaps, ladders, and stairs - UpdateLinkedEntity(position, Gap.GapList, gap => ConnectedGap = gap, gap => + if (PlayerInput.KeyDown(Keys.Space)) { - if (ConnectedGap == gap) + foreach (MapEntity e in mapEntityList) { - ConnectedGap = null; - } - }); - UpdateLinkedEntity(position, Item.ItemList, i => - { - var ladder = i?.GetComponent(); - if (ladder != null) - { - Ladders = ladder; - } - }, i => - { - var ladder = i?.GetComponent(); - if (ladder != null) - { - if (Ladders == ladder) + if (e.GetType() != typeof(WayPoint)) continue; + if (e == this) continue; + + if (!Submarine.RectContains(e.Rect, position)) continue; + + if (linkedTo.Contains(e)) { - Ladders = null; + linkedTo.Remove(e); + e.linkedTo.Remove(this); + } + else + { + linkedTo.Add(e); + e.linkedTo.Add(this); } } - }, inflate: 5); - // TODO: Cannot check the rectangle, since the rectangle is not rotated -> Need to use the collider. - //var stairList = mapEntityList.Where(me => me is Structure s && s.StairDirection != Direction.None).Select(me => me as Structure); - //UpdateLinkedEntity(position, stairList, s => - //{ - // Stairs = s; - //}, s => - //{ - // if (Stairs == s) - // { - // Stairs = null; - // } - //}); - - foreach (MapEntity e in mapEntityList) + } + else { - if (e.GetType() != typeof(WayPoint)) continue; - if (e == this) continue; - - if (!Submarine.RectContains(e.Rect, position)) continue; - - linkedTo.Add(e); - e.linkedTo.Add(this); + // Update gaps, ladders, and stairs + UpdateLinkedEntity(position, Gap.GapList, gap => ConnectedGap = gap, gap => + { + if (ConnectedGap == gap) + { + ConnectedGap = null; + } + }); + UpdateLinkedEntity(position, Item.ItemList, i => + { + var ladder = i?.GetComponent(); + if (ladder != null) + { + Ladders = ladder; + } + }, i => + { + var ladder = i?.GetComponent(); + if (ladder != null) + { + if (Ladders == ladder) + { + Ladders = null; + } + } + }, inflate: 5); + // TODO: Cannot check the rectangle, since the rectangle is not rotated -> Need to use the collider. + //var stairList = mapEntityList.Where(me => me is Structure s && s.StairDirection != Direction.None).Select(me => me as Structure); + //UpdateLinkedEntity(position, stairList, s => + //{ + // Stairs = s; + //}, s => + //{ + // if (Stairs == s) + // { + // Stairs = null; + // } + //}); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Media/Video.cs b/Barotrauma/BarotraumaClient/ClientSource/Media/Video.cs index 953104e5f..eaf0966b8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Media/Video.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Media/Video.cs @@ -1,5 +1,5 @@ using System; -using System.IO; +using Barotrauma.IO; using System.Collections.Generic; using System.Text; using System.Threading; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs index c0b8257c6..10541b14b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs @@ -17,10 +17,12 @@ namespace Barotrauma.Networking { UInt16 ID = msg.ReadUInt16(); ChatMessageType type = (ChatMessageType)msg.ReadByte(); + PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None; string txt = ""; if (type != ChatMessageType.Order) { + changeType = (PlayerConnectionChangeType)msg.ReadByte(); txt = msg.ReadString(); } @@ -114,7 +116,7 @@ namespace Barotrauma.Networking GameMain.Client.ServerSettings.ServerLog?.WriteLine(txt, messageType); break; default: - GameMain.Client.AddChatMessage(txt, type, senderName, senderCharacter); + GameMain.Client.AddChatMessage(txt, type, senderName, senderCharacter, changeType); break; } LastID = ID; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs index e0915a5af..4c754132a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChildServerRelay.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; +using Barotrauma.IO; using System.IO.Pipes; using System.Text; using System.Threading; @@ -18,8 +18,8 @@ namespace Barotrauma.Networking public static void Start(ProcessStartInfo processInfo) { - writePipe = new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable); - readPipe = new AnonymousPipeServerStream(PipeDirection.In, HandleInheritability.Inheritable); + writePipe = new AnonymousPipeServerStream(PipeDirection.Out, System.IO.HandleInheritability.Inheritable); + readPipe = new AnonymousPipeServerStream(PipeDirection.In, System.IO.HandleInheritability.Inheritable); writeStream = writePipe; readStream = readPipe; @@ -38,6 +38,13 @@ namespace Barotrauma.Networking localHandlesDisposed = true; } + public static void ClosePipes() + { + writePipe?.Close(); + readPipe?.Close(); + shutDown = true; + } + public static void ShutDown() { Process?.Kill(); Process = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs index 51fc9d4fa..7a254152f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs @@ -14,8 +14,10 @@ namespace Barotrauma.Networking public UInt64 SteamID; public byte ID; public UInt16 CharacterID; + public float Karma; public bool Muted; public bool InGame; + public bool HasPermissions; public bool AllowKicking; } @@ -44,6 +46,8 @@ namespace Barotrauma.Networking public bool AllowKicking; + public float Karma; + public void UpdateSoundPosition() { if (VoipSound == null) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs index 76d3e90a0..22a91415b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/FileTransfer/FileReceiver.cs @@ -1,7 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Threading; using System.Xml; @@ -100,7 +100,7 @@ namespace Barotrauma.Networking WriteStream = null; } - WriteStream = new FileStream(FilePath, FileMode.Create, FileAccess.Write, FileShare.None); + WriteStream = File.Open(FilePath, System.IO.FileMode.Create, System.IO.FileAccess.Write); TimeStarted = Environment.TickCount; } @@ -259,7 +259,7 @@ namespace Barotrauma.Networking { newTransfer.OpenStream(); } - catch (IOException e) + catch (System.IO.IOException e) { if (i < maxRetries) { @@ -422,7 +422,7 @@ namespace Barotrauma.Networking } if (string.IsNullOrEmpty(fileName) || - fileName.IndexOfAny(Path.GetInvalidFileNameChars()) > -1) + fileName.IndexOfAny(Path.GetInvalidFileNameChars().ToArray()) > -1) { errorMessage = "Illegal characters in file name ''" + fileName + "''"; return false; @@ -455,7 +455,7 @@ namespace Barotrauma.Networking switch (fileTransfer.FileType) { case FileTransferType.Submarine: - Stream stream; + System.IO.Stream stream; try { stream = SaveUtil.DecompressFiletoStream(fileTransfer.FilePath); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 821661faa..87983a95b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -3,7 +3,7 @@ using Barotrauma.Steam; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.IO.Compression; using System.Linq; using System.Text; @@ -28,6 +28,8 @@ namespace Barotrauma.Networking get { return name; } } + public string PendingName = string.Empty; + public void SetName(string value) { value = value.Replace(":", "").Replace(";", ""); @@ -50,7 +52,7 @@ namespace Barotrauma.Networking public GUITickBox EndVoteTickBox; private GUIComponent buttonContainer; - private NetStats netStats; + public readonly NetStats NetStats; protected GUITickBox cameraFollowsSub; @@ -112,6 +114,7 @@ namespace Barotrauma.Networking public bool SpawnAsTraitor; public string TraitorFirstObjective; + public TraitorMissionPrefab TraitorMission = null; public byte ID { @@ -166,9 +169,9 @@ namespace Barotrauma.Networking allowReconnect = true; - netStats = new NetStats(); + NetStats = new NetStats(); - inGameHUD = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), style: null) + inGameHUD = new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas), style: null) { CanBeFocused = false }; @@ -482,12 +485,13 @@ namespace Barotrauma.Networking if (requiresPw && !canStart && !connectCancelled) { + GUI.ClearCursorWait(); reconnectBox?.Close(); reconnectBox = null; string pwMsg = TextManager.Get("PasswordRequired"); var msgBox = new GUIMessageBox(pwMsg, "", new string[] { TextManager.Get("OK"), TextManager.Get("Cancel") }, - relativeSize: new Vector2(0.25f, 0.1f), minSize: new Point(400, 170)); + relativeSize: new Vector2(0.25f, 0.1f), minSize: new Point(400, (int)(170 * Math.Max(1.0f, GUI.Scale)))); var passwordHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), msgBox.Content.RectTransform), childAnchor: Anchor.TopCenter); var passwordBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1f), passwordHolder.RectTransform) { MinSize = new Point(0, 20) }) { @@ -513,6 +517,7 @@ namespace Barotrauma.Networking { requiresPw = false; connectCancelled = true; + GameMain.ServerListScreen.Select(); return true; }; @@ -564,14 +569,7 @@ namespace Barotrauma.Networking } } - /*TODO: reimplement - if (ShowNetStats && client?.ServerConnection != null) - { - netStats.AddValue(NetStats.NetStatType.ReceivedBytes, client.ServerConnection.Statistics.ReceivedBytes); - netStats.AddValue(NetStats.NetStatType.SentBytes, client.ServerConnection.Statistics.SentBytes); - netStats.AddValue(NetStats.NetStatType.ResentMessages, client.ServerConnection.Statistics.ResentMessages); - netStats.Update(deltaTime); - }*/ + NetStats.Update(deltaTime); UpdateHUD(deltaTime); @@ -669,6 +667,7 @@ namespace Barotrauma.Networking if (header != ServerPacketHeader.STARTGAMEFINALIZE && header != ServerPacketHeader.ENDGAME && + header != ServerPacketHeader.PING_REQUEST && roundInitStatus == RoundInitStatus.WaitingForStartGameFinalize) { //rewind the header byte we just read @@ -679,6 +678,31 @@ namespace Barotrauma.Networking switch (header) { + case ServerPacketHeader.PING_REQUEST: + IWriteMessage response = new WriteOnlyMessage(); + response.Write((byte)ClientPacketHeader.PING_RESPONSE); + byte requestLen = inc.ReadByte(); + response.Write(requestLen); + for (int i=0;i c.ID == clientId); + if (client != null) + { + client.Ping = clientPing; + } + } + break; case ServerPacketHeader.UPDATE_LOBBY: ReadLobbyUpdate(inc); break; @@ -750,7 +774,7 @@ namespace Barotrauma.Networking if (readyToStart && !CoroutineManager.IsCoroutineRunning("WaitForStartRound")) { - CoroutineManager.StartCoroutine(GameMain.NetLobbyScreen.WaitForStartRound(startButton: null, allowCancel: false), "WaitForStartRound"); + CoroutineManager.StartCoroutine(GameMain.NetLobbyScreen.WaitForStartRound(startButton: null), "WaitForStartRound"); } break; case ServerPacketHeader.STARTGAME: @@ -1056,11 +1080,13 @@ namespace Barotrauma.Networking var missionPrefab = TraitorMissionPrefab.List.Find(t => t.Identifier == missionIdentifier); Sprite icon = missionPrefab?.Icon; - switch(messageType) { + switch(messageType) + { case TraitorMessageType.Objective: var isTraitor = !string.IsNullOrEmpty(message); SpawnAsTraitor = isTraitor; TraitorFirstObjective = message; + TraitorMission = missionPrefab; if (Character != null) { Character.IsTraitor = isTraitor; @@ -1422,6 +1448,15 @@ namespace Barotrauma.Networking var teamID = i == 0 ? Character.TeamType.Team1 : Character.TeamType.Team2; Submarine.MainSubs[i].TeamID = teamID; + foreach (Item item in Item.ItemList) + { + if (item.Submarine == null) { continue; } + if (item.Submarine != Submarine.MainSubs[i] && !Submarine.MainSubs[i].DockedTo.Contains(item.Submarine)) { continue; } + foreach (WifiComponent wifiComponent in item.GetComponents()) + { + wifiComponent.TeamID = Submarine.MainSubs[i].TeamID; + } + } foreach (Submarine sub in Submarine.MainSubs[i].DockedTo) { sub.TeamID = teamID; @@ -1503,7 +1538,7 @@ namespace Barotrauma.Networking var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.Hash == subHash) ?? - new SubmarineInfo(Path.Combine(SubmarineInfo.SavePath, subName) + ".sub", subHash); + new SubmarineInfo(Path.Combine(SubmarineInfo.SavePath, subName) + ".sub", subHash, tryLoad: false); matchingSub.RequiredContentPackagesInstalled = requiredContentPackagesInstalled; serverSubmarines.Add(matchingSub); @@ -1537,9 +1572,11 @@ namespace Barotrauma.Networking string name = inc.ReadString(); string preferredJob = inc.ReadString(); UInt16 characterID = inc.ReadUInt16(); + float karma = inc.ReadSingle(); bool muted = inc.ReadBoolean(); bool inGame = inc.ReadBoolean(); - bool allowKicking = inc.ReadBoolean(); + bool hasPermissions = inc.ReadBoolean(); + bool allowKicking = inc.ReadBoolean() || IsServerOwner; inc.ReadPadBits(); tempClients.Add(new TempClient @@ -1550,8 +1587,10 @@ namespace Barotrauma.Networking Name = name, PreferredJob = preferredJob, CharacterID = characterID, + Karma = karma, Muted = muted, InGame = inGame, + HasPermissions = hasPermissions, AllowKicking = allowKicking }); } @@ -1579,7 +1618,9 @@ namespace Barotrauma.Networking existingClient.NameID = tc.NameID; existingClient.PreferredJob = tc.PreferredJob; existingClient.Character = null; + existingClient.Karma = tc.Karma; existingClient.Muted = tc.Muted; + existingClient.HasPermissions = tc.HasPermissions; existingClient.InGame = tc.InGame; existingClient.AllowKicking = tc.AllowKicking; GameMain.NetLobbyScreen.SetPlayerNameAndJobPreference(existingClient); @@ -1852,8 +1893,8 @@ namespace Barotrauma.Networking DebugConsole.ThrowError("Writing object data to \"crashreport_object.bin\", please send this file to us at http://github.com/Regalis11/Barotrauma/issues"); - using (FileStream fl = File.Open("crashreport_object.bin", FileMode.Create)) - using (BinaryWriter sw = new BinaryWriter(fl)) + using (FileStream fl = File.Open("crashreport_object.bin", System.IO.FileMode.Create)) + using (System.IO.BinaryWriter sw = new System.IO.BinaryWriter(fl)) { sw.Write(inc.Buffer, (int)(prevBytePos - prevByteLength), (int)(prevByteLength)); } @@ -2085,7 +2126,8 @@ namespace Barotrauma.Networking GameMain.GameSession.SubmarineInfo = new SubmarineInfo(subPath, ""); } SaveUtil.LoadGame(GameMain.GameSession.SavePath, GameMain.GameSession); - GameMain.GameSession?.Submarine?.CheckSubsLeftBehind(); + GameMain.GameSession?.SubmarineInfo?.Reload(); + GameMain.GameSession?.SubmarineInfo?.CheckSubsLeftBehind(); if (GameMain.GameSession?.SubmarineInfo?.Name != null) { GameMain.NetLobbyScreen.TryDisplayCampaignSubmarine(GameMain.GameSession.SubmarineInfo); @@ -2540,6 +2582,11 @@ namespace Barotrauma.Networking inGameHUD.AddToGUIUpdateList(); GameMain.NetLobbyScreen.FileTransferFrame?.AddToGUIUpdateList(); } + + serverSettings.AddToGUIUpdateList(); + if (serverSettings.ServerLog.LogFrame != null) serverSettings.ServerLog.LogFrame.AddToGUIUpdateList(); + + GameMain.NetLobbyScreen?.PlayerFrame?.AddToGUIUpdateList(); } public void UpdateHUD(float deltaTime) @@ -2583,7 +2630,7 @@ namespace Barotrauma.Networking if (GUI.KeyboardDispatcher.Subscriber == null) { bool chatKeyHit = PlayerInput.KeyHit(InputType.Chat); - bool radioKeyHit = PlayerInput.KeyHit(InputType.RadioChat) && (Character.Controlled == null || Character.Controlled.SpeechImpediment < 0); + bool radioKeyHit = PlayerInput.KeyHit(InputType.RadioChat) && (Character.Controlled == null || Character.Controlled.SpeechImpediment < 100); if (chatKeyHit || radioKeyHit) { @@ -2629,8 +2676,6 @@ namespace Barotrauma.Networking } } } - serverSettings.AddToGUIUpdateList(); - if (serverSettings.ServerLog.LogFrame != null) serverSettings.ServerLog.LogFrame.AddToGUIUpdateList(); } public virtual void Draw(Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch) @@ -2716,15 +2761,15 @@ namespace Barotrauma.Networking if (!ShowNetStats) return; - netStats.Draw(spriteBatch, new Rectangle(300, 10, 300, 150)); + NetStats.Draw(spriteBatch, new Rectangle(300, 10, 300, 150)); + /* TODO: reimplement int width = 200, height = 300; int x = GameMain.GraphicsWidth - width, y = (int)(GameMain.GraphicsHeight * 0.3f); GUI.DrawRectangle(spriteBatch, new Rectangle(x, y, width, height), Color.Black * 0.7f, true); GUI.Font.DrawString(spriteBatch, "Network statistics:", new Vector2(x + 10, y + 10), Color.White); - /* TODO: reimplement if (client.ServerConnection != null) { GUI.Font.DrawString(spriteBatch, "Ping: " + (int)(client.ServerConnection.AverageRoundtripTime * 1000.0f) + " ms", new Vector2(x + 10, y + 25), Color.White); @@ -2743,73 +2788,106 @@ namespace Barotrauma.Networking }*/ } - public virtual bool SelectCrewCharacter(Character character, GUIComponent characterFrame) + public virtual bool SelectCrewCharacter(Character character, GUIComponent frame) { - if (character == null) { return false; } + if (character == null) return false; if (character != myCharacter) { var client = GameMain.NetworkMember.ConnectedClients.Find(c => c.Character == character); - if (client == null) { return false; } + if (client == null) return false; - var content = new GUIFrame(new RectTransform(new Vector2(0.9f, 1.0f - characterFrame.RectTransform.RelativeSize.Y), characterFrame.RectTransform, Anchor.BottomCenter, Pivot.TopCenter), - style: null); - - var mute = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform, Anchor.TopCenter), - TextManager.Get("Mute")) - { - Selected = client.MutedLocally, - OnSelected = (tickBox) => { client.MutedLocally = tickBox.Selected; return true; } - }; - - var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform, Anchor.BottomCenter), isHorizontal: true) - { - RelativeSpacing = 0.05f, - ChildAnchor = Anchor.CenterLeft, - Stretch = true - }; - - if (HasPermission(ClientPermissions.Ban)) - { - var banButton = new GUIButton(new RectTransform(new Vector2(0.45f, 0.9f), buttonContainer.RectTransform), - TextManager.Get("Ban"), style: "GUIButtonSmall") - { - UserData = client, - OnClicked = (btn, userdata) => { GameMain.NetLobbyScreen.BanPlayer(client); return false; } - }; - } - if (HasPermission(ClientPermissions.Kick)) - { - var kickButton = new GUIButton(new RectTransform(new Vector2(0.45f, 0.9f), buttonContainer.RectTransform), - TextManager.Get("Kick"), style: "GUIButtonSmall") - { - UserData = client, - OnClicked = (btn, userdata) => { GameMain.NetLobbyScreen.KickPlayer(client); return false; } - }; - } - else if (serverSettings.Voting.AllowVoteKick) - { - var kickVoteButton = new GUIButton(new RectTransform(new Vector2(0.45f, 0.9f), buttonContainer.RectTransform), - TextManager.Get("VoteToKick"), style: "GUIButtonSmall") - { - UserData = client, - OnClicked = (btn, userdata) => { VoteForKick(client); btn.Enabled = false; return true; } - }; - if (GameMain.NetworkMember.ConnectedClients != null) - { - kickVoteButton.Enabled = !client.HasKickVoteFromID(myID); - } - } + CreateSelectionRelatedButtons(client, frame); } return true; } + public virtual bool SelectCrewClient(Client client, GUIComponent frame) + { + if (client == null || client.ID == ID) return false; + CreateSelectionRelatedButtons(client, frame); + return true; + } + + private void CreateSelectionRelatedButtons(Client client, GUIComponent frame) + { + var content = new GUIFrame(new RectTransform(new Vector2(1f, 1.0f - frame.RectTransform.RelativeSize.Y), frame.RectTransform, Anchor.BottomCenter, Pivot.TopCenter), + style: null); + + var mute = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform, Anchor.TopCenter), + TextManager.Get("Mute")) + { + Selected = client.MutedLocally, + OnSelected = (tickBox) => { client.MutedLocally = tickBox.Selected; return true; } + }; + + var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.35f), content.RectTransform, Anchor.BottomCenter), isHorizontal: true, childAnchor: Anchor.BottomLeft) + { + RelativeSpacing = 0.05f, + Stretch = true + }; + + if (!GameMain.Client.GameStarted || (GameMain.Client.Character == null || GameMain.Client.Character.IsDead) && (client.Character == null || client.Character.IsDead)) + { + var messageButton = new GUIButton(new RectTransform(new Vector2(1f, 0.2f), content.RectTransform, Anchor.BottomCenter) { RelativeOffset = new Vector2(0f, buttonContainer.RectTransform.RelativeSize.Y) }, + TextManager.Get("message"), style: "GUIButtonSmall") + { + UserData = client, + OnClicked = (btn, userdata) => + { + chatBox.InputBox.Text = $"{client.Name}; "; + CoroutineManager.StartCoroutine(selectCoroutine()); + return false; + } + }; + } + + // Need a delayed selection due to the inputbox being deselected when a left click occurs outside of it + IEnumerable selectCoroutine() + { + yield return new WaitForSeconds(0.01f, true); + chatBox.InputBox.Select(chatBox.InputBox.Text.Length); + } + + if (HasPermission(ClientPermissions.Ban) && client.AllowKicking) + { + var banButton = new GUIButton(new RectTransform(new Vector2(0.45f, 0.9f), buttonContainer.RectTransform), + TextManager.Get("Ban"), style: "GUIButtonSmall") + { + UserData = client, + OnClicked = (btn, userdata) => { GameMain.NetLobbyScreen.BanPlayer(client); return false; } + }; + } + if (HasPermission(ClientPermissions.Kick) && client.AllowKicking) + { + var kickButton = new GUIButton(new RectTransform(new Vector2(0.45f, 0.9f), buttonContainer.RectTransform), + TextManager.Get("Kick"), style: "GUIButtonSmall") + { + UserData = client, + OnClicked = (btn, userdata) => { GameMain.NetLobbyScreen.KickPlayer(client); return false; } + }; + } + else if (serverSettings.Voting.AllowVoteKick && client.AllowKicking) + { + var kickVoteButton = new GUIButton(new RectTransform(new Vector2(0.45f, 0.9f), buttonContainer.RectTransform), + TextManager.Get("VoteToKick"), style: "GUIButtonSmall") + { + UserData = client, + OnClicked = (btn, userdata) => { VoteForKick(client); btn.Enabled = false; return true; } + }; + if (GameMain.NetworkMember.ConnectedClients != null) + { + kickVoteButton.Enabled = !client.HasKickVoteFromID(myID); + } + } + } + public void CreateKickReasonPrompt(string clientName, bool ban, bool rangeBan = false) { var banReasonPrompt = new GUIMessageBox( TextManager.Get(ban ? "BanReasonPrompt" : "KickReasonPrompt"), - "", new string[] { TextManager.Get("OK"), TextManager.Get("Cancel") }, new Vector2(0.25f, 0.2f), new Point(400, 200)); + "", new string[] { TextManager.Get("OK"), TextManager.Get("Cancel") }, new Vector2(0.25f, 0.22f), new Point(400, 220)); var content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.6f), banReasonPrompt.InnerFrame.RectTransform, Anchor.Center)); var banReasonBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.3f), content.RectTransform)) @@ -2823,14 +2901,16 @@ namespace Barotrauma.Networking if (ban) { - new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.15f), content.RectTransform), TextManager.Get("BanDuration")); - permaBanTickBox = new GUITickBox(new RectTransform(new Vector2(0.8f, 0.15f), content.RectTransform) { RelativeOffset = new Vector2(0.05f, 0.0f) }, - TextManager.Get("BanPermanent")) + + var labelContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.25f), content.RectTransform), isHorizontal: false); + new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), labelContainer.RectTransform), TextManager.Get("BanDuration")) { Padding = Vector4.Zero }; + var buttonContent = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), labelContainer.RectTransform), isHorizontal: true); + permaBanTickBox = new GUITickBox(new RectTransform(new Vector2(0.4f, 0.15f), buttonContent.RectTransform), TextManager.Get("BanPermanent")) { Selected = true }; - var durationContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.15f), content.RectTransform), isHorizontal: true) + var durationContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 1f), buttonContent.RectTransform), isHorizontal: true) { Visible = false }; @@ -2930,6 +3010,11 @@ namespace Barotrauma.Networking if (GameMain.GameSession?.GameMode != null) { errorLines.Add("Game mode: " + GameMain.GameSession.GameMode.Name); + if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) + { + errorLines.Add("Campaign ID: " + campaign.CampaignID); + errorLines.Add("Campaign save ID: " + campaign.LastSaveID + "(pending: " + campaign.PendingSaveID + ")"); + } } if (GameMain.GameSession?.Submarine != null) { @@ -2938,6 +3023,13 @@ namespace Barotrauma.Networking if (Level.Loaded != null) { errorLines.Add("Level: " + Level.Loaded.Seed + ", " + Level.Loaded.EqualityCheckVal); + errorLines.Add("Entity count before generating level: " + Level.Loaded.EntityCountBeforeGenerate); + errorLines.Add("Entities:"); + foreach (Entity e in Level.Loaded.EntitiesBeforeGenerate) + { + errorLines.Add(" " + e.ID + ": " + e.ToString()); + } + errorLines.Add("Entity count after generating level: " + Level.Loaded.EntityCountAfterGenerate); } errorLines.Add("Entity IDs:"); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs index 4f1bc98e0..b279d2775 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs @@ -47,11 +47,30 @@ namespace Barotrauma CreateLabeledSlider(parent, 0.0f, 1.0f, 0.01f, nameof(StructureDamageKarmaDecrease)); CreateLabeledSlider(parent, 0.0f, 1.0f, 0.01f, nameof(DamageFriendlyKarmaDecrease)); + //hide these for now if a localized text is not available + if (TextManager.ContainsTag("Karma." + nameof(StunFriendlyKarmaDecrease))) + { + CreateLabeledSlider(parent, 0.0f, 1.0f, 0.01f, nameof(StunFriendlyKarmaDecrease)); + } + if (TextManager.ContainsTag("Karma." + nameof(StunFriendlyKarmaDecreaseThreshold))) + { + CreateLabeledSlider(parent, 0.0f, 10.0f, 1.0f, nameof(StunFriendlyKarmaDecreaseThreshold)); + } CreateLabeledSlider(parent, 0.0f, 100.0f, 1.0f, nameof(ReactorMeltdownKarmaDecrease)); CreateLabeledSlider(parent, 0.0f, 10.0f, 0.05f, nameof(ReactorOverheatKarmaDecrease)); CreateLabeledNumberInput(parent, 0, 20, nameof(AllowedWireDisconnectionsPerMinute)); CreateLabeledSlider(parent, 0.0f, 20.0f, 0.5f, nameof(WireDisconnectionKarmaDecrease)); CreateLabeledSlider(parent, 0.0f, 30.0f, 1.0f, nameof(SpamFilterKarmaDecrease)); + + //hide these for now if a localized text is not available + if (TextManager.ContainsTag("Karma." + nameof(DangerousItemStealKarmaDecrease))) + { + CreateLabeledSlider(parent, 0.0f, 30.0f, 1.0f, nameof(DangerousItemStealKarmaDecrease)); + } + if (TextManager.ContainsTag("Karma." + nameof(DangerousItemStealBots))) + { + CreateLabeledTickBox(parent, nameof(DangerousItemStealBots)); + } } private void CreateLabeledSlider(GUIComponent parent, float min, float max, float step, string propertyName) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetStats.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetStats.cs index 90cd0a00e..eb34d3cdd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetStats.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetStats.cs @@ -14,10 +14,10 @@ namespace Barotrauma.Networking ResentMessages = 2 } - private Graph[] graphs; + private readonly Graph[] graphs; - private float[] totalValue; - private float[] lastValue; + private readonly float[] totalValue; + private readonly float[] lastValue; const float UpdateInterval = 0.1f; float updateTimer; @@ -37,9 +37,7 @@ namespace Barotrauma.Networking public void AddValue(NetStatType statType, float value) { float valueChange = value - lastValue[(int)statType]; - totalValue[(int)statType] += valueChange; - lastValue[(int)statType] = value; } @@ -51,7 +49,6 @@ namespace Barotrauma.Networking for (int i = 0; i < 3; i++) { - graphs[i].Update(totalValue[i] / UpdateInterval); totalValue[i] = 0.0f; } @@ -64,23 +61,22 @@ namespace Barotrauma.Networking GUI.DrawRectangle(spriteBatch, rect, Color.Black * 0.4f, true); graphs[(int)NetStatType.ReceivedBytes].Draw(spriteBatch, rect, null, 0.0f, Color.Cyan); - graphs[(int)NetStatType.SentBytes].Draw(spriteBatch, rect, null, 0.0f, GUI.Style.Orange); - - graphs[(int)NetStatType.ResentMessages].Draw(spriteBatch, rect, null, 0.0f, GUI.Style.Red); + if (graphs[(int)NetStatType.ResentMessages].Average() > 0) + { + graphs[(int)NetStatType.ResentMessages].Draw(spriteBatch, rect, null, 0.0f, GUI.Style.Red); + GUI.SmallFont.DrawString(spriteBatch, "Peak resent: " + graphs[(int)NetStatType.ResentMessages].LargestValue() + " messages/s", + new Vector2(rect.Right + 10, rect.Y + 50), GUI.Style.Red); + } GUI.SmallFont.DrawString(spriteBatch, "Peak received: " + MathUtils.GetBytesReadable((int)graphs[(int)NetStatType.ReceivedBytes].LargestValue()) + "/s " + "Avg received: " + MathUtils.GetBytesReadable((int)graphs[(int)NetStatType.ReceivedBytes].Average()) + "/s", new Vector2(rect.Right + 10, rect.Y + 10), Color.Cyan); - GUI.SmallFont.DrawString(spriteBatch, "Peak sent: " + MathUtils.GetBytesReadable((int)graphs[(int)NetStatType.SentBytes].LargestValue()) + "/s " + "Avg sent: " + MathUtils.GetBytesReadable((int)graphs[(int)NetStatType.SentBytes].Average()) + "/s", new Vector2(rect.Right + 10, rect.Y + 30), GUI.Style.Orange); - - GUI.SmallFont.DrawString(spriteBatch, "Peak resent: " + graphs[(int)NetStatType.ResentMessages].LargestValue() + " messages/s", - new Vector2(rect.Right + 10, rect.Y + 50), GUI.Style.Red); #if DEBUG /*int y = 10; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs index 41f8f65af..58cf2f388 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs @@ -39,7 +39,10 @@ namespace Barotrauma.Networking contentPackageOrderReceived = false; - netPeerConfiguration = new NetPeerConfiguration("barotrauma"); + netPeerConfiguration = new NetPeerConfiguration("barotrauma") + { + UseDualModeSockets = GameMain.Config.UseDualModeSockets + }; netPeerConfiguration.DisableMessageType(NetIncomingMessageType.DebugMessage | NetIncomingMessageType.WarningMessage | NetIncomingMessageType.Receipt | NetIncomingMessageType.ErrorMessage | NetIncomingMessageType.Error); @@ -94,6 +97,9 @@ namespace Barotrauma.Networking incomingLidgrenMessages.Clear(); netClient.ReadMessages(incomingLidgrenMessages); + GameMain.Client?.NetStats?.AddValue(NetStats.NetStatType.ReceivedBytes, netClient.Statistics.ReceivedBytes); + GameMain.Client?.NetStats?.AddValue(NetStats.NetStatType.SentBytes, netClient.Statistics.SentBytes); + foreach (NetIncomingMessage inc in incomingLidgrenMessages) { if (inc.SenderConnection != (ServerConnection as LidgrenConnection).NetConnection) { continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs index d8a94ef28..ff72730d9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs @@ -18,6 +18,8 @@ namespace Barotrauma.Networking private double timeout; private double heartbeatTimer; + private long sentBytes, receivedBytes; + private List incomingInitializationMessages; private List incomingDataMessages; @@ -63,6 +65,7 @@ namespace Barotrauma.Networking outMsg.Write((byte)ConnectionInitialization.ConnectionStarted); Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable); + sentBytes += outMsg.LengthBytes; initializationStep = ConnectionInitialization.SteamTicketAndVersion; @@ -99,11 +102,11 @@ namespace Barotrauma.Networking if (isConnectionInitializationStep) { ulong low = Lidgren.Network.NetBitWriter.ReadUInt32(data, 32, 8); - ulong high = Lidgren.Network.NetBitWriter.ReadUInt32(data, 32, 8+32); + ulong high = Lidgren.Network.NetBitWriter.ReadUInt32(data, 32, 8 + 32); ulong lobbyId = low + (high << 32); Steam.SteamManager.JoinLobby(lobbyId, false); - IReadMessage inc = new ReadOnlyMessage(data, false, 1+8, dataLength - 9, ServerConnection); + IReadMessage inc = new ReadOnlyMessage(data, false, 1 + 8, dataLength - 9, ServerConnection); if (initializationStep != ConnectionInitialization.Success) { incomingInitializationMessages.Add(inc); @@ -137,15 +140,20 @@ namespace Barotrauma.Networking timeout -= deltaTime; heartbeatTimer -= deltaTime; - while (Steamworks.SteamNetworking.IsP2PPacketAvailable()) + for (int i = 0; i < 100; i++) { + if (!Steamworks.SteamNetworking.IsP2PPacketAvailable()) { break; } var packet = Steamworks.SteamNetworking.ReadP2PPacket(); if (packet.HasValue) { OnP2PData(packet?.SteamId ?? 0, packet?.Data, packet?.Data.Length ?? 0, 0); + receivedBytes += packet?.Data.Length ?? 0; } } + GameMain.Client?.NetStats?.AddValue(NetStats.NetStatType.ReceivedBytes, receivedBytes); + GameMain.Client?.NetStats?.AddValue(NetStats.NetStatType.SentBytes, sentBytes); + if (heartbeatTimer < 0.0) { IWriteMessage outMsg = new WriteOnlyMessage(); @@ -153,6 +161,7 @@ namespace Barotrauma.Networking outMsg.Write((byte)PacketHeader.IsHeartbeatMessage); Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Unreliable); + sentBytes += outMsg.LengthBytes; heartbeatTimer = 5.0; } @@ -226,6 +235,7 @@ namespace Barotrauma.Networking heartbeatTimer = 5.0; Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable); + sentBytes += outMsg.LengthBytes; break; case ConnectionInitialization.ContentPackageOrder: if (initializationStep == ConnectionInitialization.SteamTicketAndVersion || @@ -253,7 +263,7 @@ namespace Barotrauma.Networking } Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable); - + sentBytes += outMsg.LengthBytes; break; case ConnectionInitialization.Password: if (initializationStep == ConnectionInitialization.SteamTicketAndVersion) { initializationStep = ConnectionInitialization.Password; } @@ -333,6 +343,7 @@ namespace Barotrauma.Networking private void Send(byte[] buf, int length, Steamworks.P2PSend sendType) { bool successSend = Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, buf, length + 4, 0, sendType); + sentBytes += length + 4; if (!successSend) { if (sendType != Steamworks.P2PSend.Reliable) @@ -340,6 +351,7 @@ namespace Barotrauma.Networking DebugConsole.Log("WARNING: message couldn't be sent unreliably, forcing reliable send (" + length.ToString() + " bytes)"); sendType = Steamworks.P2PSend.Reliable; successSend = Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, buf, length + 4, 0, sendType); + sentBytes += length + 4; } if (!successSend) { @@ -363,6 +375,7 @@ namespace Barotrauma.Networking heartbeatTimer = 5.0; Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable); + sentBytes += outMsg.LengthBytes; } public override void Close(string msg = null) @@ -379,6 +392,7 @@ namespace Barotrauma.Networking outMsg.Write(msg ?? "Disconnected"); Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable); + sentBytes += outMsg.LengthBytes; Thread.Sleep(100); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs index cf5534b9e..5bc6e9ec2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs @@ -13,7 +13,9 @@ namespace Barotrauma.Networking private bool isActive; private ConnectionInitialization initializationStep; - private UInt64 selfSteamID; + private readonly UInt64 selfSteamID; + + private long sentBytes, receivedBytes; class RemotePeer { @@ -204,22 +206,24 @@ namespace Barotrauma.Networking } } - for (int i=0;i<100;i++) + for (int i = 0; i < 100; i++) { if (!Steamworks.SteamNetworking.IsP2PPacketAvailable()) { break; } var packet = Steamworks.SteamNetworking.ReadP2PPacket(); if (packet.HasValue) { OnP2PData(packet?.SteamId ?? 0, packet?.Data, packet?.Data.Length ?? 0, 0); + receivedBytes += packet?.Data.Length ?? 0; } } + GameMain.Client?.NetStats?.AddValue(NetStats.NetStatType.ReceivedBytes, receivedBytes); + GameMain.Client?.NetStats?.AddValue(NetStats.NetStatType.SentBytes, sentBytes); + while (ChildServerRelay.Read(out byte[] incBuf)) { ChildServerRelay.DisposeLocalHandles(); - IReadMessage inc = new ReadOnlyMessage(incBuf, false, 0, incBuf.Length, ServerConnection); - HandleDataMessage(inc); } } @@ -295,6 +299,7 @@ namespace Barotrauma.Networking } bool successSend = Steamworks.SteamNetworking.SendP2PPacket(recipientSteamId, p2pData, p2pData.Length, 0, sendType); + sentBytes += p2pData.Length; if (!successSend) { @@ -303,6 +308,7 @@ namespace Barotrauma.Networking DebugConsole.Log("WARNING: message couldn't be sent unreliably, forcing reliable send (" + p2pData.Length.ToString() + " bytes)"); sendType = Steamworks.P2PSend.Reliable; successSend = Steamworks.SteamNetworking.SendP2PPacket(recipientSteamId, p2pData, p2pData.Length, 0, sendType); + sentBytes += p2pData.Length; } if (!successSend) { @@ -336,7 +342,6 @@ namespace Barotrauma.Networking byte[] msgToSend = (byte[])outMsg.Buffer.Clone(); Array.Resize(ref msgToSend, outMsg.LengthBytes); ChildServerRelay.Write(msgToSend); - return; } else @@ -369,6 +374,7 @@ namespace Barotrauma.Networking outMsg.Write(msg); Steamworks.SteamNetworking.SendP2PPacket(peer.SteamID, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable); + sentBytes += outMsg.LengthBytes; } else { @@ -405,7 +411,7 @@ namespace Barotrauma.Networking ClosePeerSession(remotePeers[i]); } - ChildServerRelay.ShutDown(); + ChildServerRelay.ClosePipes(); OnDisconnect?.Invoke(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs index 229e0c7b1..0e1f02597 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs @@ -2,6 +2,8 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.Xna.Framework.Graphics; +using Barotrauma.Extensions; namespace Barotrauma.Networking { @@ -9,15 +11,28 @@ namespace Barotrauma.Networking { public GUIButton LogFrame; private GUIListBox listBox; + private GUIButton reverseButton; private string msgFilter; + private bool reverseOrder = false; + + private bool OnReverseClicked(GUIButton btn, object obj) + { + SetMessageReversal(!reverseOrder); + + return false; + } + public void CreateLogFrame() { - LogFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas), style: "GUIBackgroundBlocker") + LogFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null) { OnClicked = (btn, userdata) => { if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) LogFrame = null; return true; } }; + + new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, LogFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); + new GUIButton(new RectTransform(Vector2.One, LogFrame.RectTransform), "", style: null).OnClicked += (btn, userData) => { LogFrame = null; @@ -80,7 +95,17 @@ namespace Barotrauma.Networking GUI.KeyboardDispatcher.Subscriber = searchBox; filterArea.RectTransform.MinSize = new Point(0, filterArea.RectTransform.Children.Max(c => c.MinSize.Y)); - listBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.95f), rightColumn.RectTransform)); + GUILayoutGroup listBoxLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.95f), rightColumn.RectTransform)) + { + Stretch = true, + RelativeSpacing = 0.0f + }; + + reverseButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), listBoxLayout.RectTransform), style: "UIToggleButtonVertical"); + reverseButton.Children.ForEach(c => c.SpriteEffects = reverseOrder ? SpriteEffects.FlipVertically : SpriteEffects.None); + reverseButton.OnClicked = OnReverseClicked; + + listBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.95f), listBoxLayout.RectTransform)); GUIButton closeButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.05f), rightColumn.RectTransform), TextManager.Get("Close")) { @@ -107,7 +132,7 @@ namespace Barotrauma.Networking msgFilter = ""; } - public void AssignLogFrame(GUIListBox inListBox, GUIComponent tickBoxContainer, GUITextBox searchBox) + public void AssignLogFrame(GUIButton inReverseButton, GUIListBox inListBox, GUIComponent tickBoxContainer, GUITextBox searchBox) { searchBox.OnTextChanged += (textBox, text) => { @@ -144,6 +169,10 @@ namespace Barotrauma.Networking inListBox.ClearChildren(); listBox = inListBox; + reverseButton = inReverseButton; + reverseButton.Children.ForEach(c => c.SpriteEffects = reverseOrder ? SpriteEffects.FlipVertically : SpriteEffects.None); + reverseButton.OnClicked = OnReverseClicked; + var currLines = lines.ToList(); foreach (LogMessage line in currLines) { @@ -158,8 +187,34 @@ namespace Barotrauma.Networking { 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) + GUIFrame textContainer = null; + + Anchor anchor = Anchor.TopLeft; + Pivot pivot = Pivot.TopLeft; + if (line.RichData != null) + { + foreach (var data in line.RichData) + { + UInt64 id = 0; + if (!UInt64.TryParse(data.Metadata, out id)) { return; } + Client client = GameMain.Client.ConnectedClients.Find(c => c.SteamID == id); + client ??= GameMain.Client.ConnectedClients.Find(c => c.ID == id); + if (client != null && client.Karma < 40.0f) + { + textContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), listBox.Content.RectTransform), + style: null, color: new Color(0xff111155)) + { + CanBeFocused = false + }; + anchor = Anchor.CenterLeft; + pivot = Pivot.CenterLeft; + break; + } + } + } + + var textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), (textContainer ?? listBox.Content).RectTransform, anchor, pivot), + line.RichData, line.SanitizedText, wrap: true, font: GUI.SmallFont) { TextColor = messageColor[line.Type], Visible = !msgTypeHidden[(int)line.Type], @@ -167,6 +222,38 @@ namespace Barotrauma.Networking UserData = line }; + if (textContainer != null) + { + textContainer.RectTransform.NonScaledSize = new Point(textContainer.RectTransform.NonScaledSize.X, textBlock.RectTransform.NonScaledSize.Y + 5); + textBlock.SetTextPos(); + textBlock.RectTransform.Resize(textContainer.RectTransform.NonScaledSize); + } + + if (reverseOrder) + { + textBlock.RectTransform.SetAsFirstChild(); + } + + if (line.RichData != null) + { + foreach (var data in line.RichData) + { + textBlock.ClickableAreas.Add(new GUITextBlock.ClickableArea() + { + Data = data, + OnClick = (component, area) => + { + UInt64 id = 0; + if (!UInt64.TryParse(area.Data.Metadata, out id)) { return; } + Client client = GameMain.Client.ConnectedClients.Find(c => c.SteamID == id); + client ??= GameMain.Client.ConnectedClients.Find(c => c.ID == id); + if (client == null) { return; } + GameMain.NetLobbyScreen.SelectPlayer(client); + } + }); + } + } + if ((prevSize == 1.0f && listBox.BarScroll == 0.0f) || (prevSize < 1.0f && listBox.BarScroll == 1.0f)) listBox.BarScroll = 1.0f; } @@ -195,6 +282,16 @@ namespace Barotrauma.Networking return true; } + private void SetMessageReversal(bool reverse) + { + if (reverseOrder == reverse) { return; } + + reverseOrder = reverse; + reverseButton.Children.ForEach(c => c.SpriteEffects = reverseOrder ? SpriteEffects.FlipVertically : SpriteEffects.None); + + listBox.Content.RectTransform.ReverseChildren(); + } + public bool ClearFilter(GUIComponent button, object obj) { var searchBox = button.UserData as GUITextBox; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index c299c8380..22f0dc0a1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -266,7 +266,9 @@ namespace Barotrauma.Networking } //background frame - settingsFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), style: null, color: Color.Black * 0.5f); + settingsFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null); + new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, settingsFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); + new GUIButton(new RectTransform(Vector2.One, settingsFrame.RectTransform), "", style: null).OnClicked += (btn, userData) => { if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) { ToggleSettingsFrame(btn, userData); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs index 886e62140..299cb6d00 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs @@ -2,14 +2,14 @@ using RestSharp; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using RestSharp.Contrib; using System.Xml.Linq; -using System.Xml; using Color = Microsoft.Xna.Framework.Color; +using System.Runtime.InteropServices; namespace Barotrauma.Steam { @@ -37,6 +37,11 @@ namespace Barotrauma.Steam popularTags.Insert(i, commonness.Key); i++; } + + LogSteamworksNetworkingDelegate = LogSteamworksNetworking; + + IntPtr logSteamworksNetworkingPtr = Marshal.GetFunctionPointerForDelegate(LogSteamworksNetworkingDelegate); + Steamworks.SteamNetworkingUtils.SetDebugOutputFunction(Steamworks.Data.DebugOutputType.Everything, logSteamworksNetworkingPtr); } } catch (DllNotFoundException) @@ -63,6 +68,15 @@ namespace Barotrauma.Steam } } + public static bool NetworkingDebugLog = false; + + private static Steamworks.Data.FSteamNetworkingSocketsDebugOutput LogSteamworksNetworkingDelegate; + + private static void LogSteamworksNetworking(Steamworks.Data.DebugOutputType nType, string pszMsg) + { + if (NetworkingDebugLog) { DebugConsole.NewMessage($"({nType}) {pszMsg}", Color.Orange); } + } + private static void UpdateProjectSpecific(float deltaTime) { if (ugcSubscriptionTasks != null) @@ -235,7 +249,8 @@ namespace Barotrauma.Steam } }; - Steamworks.Data.LobbyQuery lobbyQuery = Steamworks.SteamMatchmaking.CreateLobbyQuery().FilterDistanceWorldwide(); + //TODO: find a better strategy to fetch all lobbies, this is gonna take forever if we actually have 10000 lobbies + Steamworks.Data.LobbyQuery lobbyQuery = Steamworks.SteamMatchmaking.CreateLobbyQuery().FilterDistanceWorldwide().WithMaxResults(10000); TaskPool.Add(Task.Run(async () => { @@ -355,7 +370,7 @@ namespace Barotrauma.Steam if (Enum.TryParse(lobby.GetData("traitors"), out YesNoMaybe traitorsEnabled)) { serverInfo.TraitorsEnabled = traitorsEnabled; } serverInfo.GameStarted = lobby.GetData("gamestarted") == "True"; - serverInfo.GameMode = lobby.GetData("gamemode"); + serverInfo.GameMode = lobby.GetData("gamemode") ?? ""; if (Enum.TryParse(lobby.GetData("playstyle"), out PlayStyle playStyle)) serverInfo.PlayStyle = playStyle; if (serverInfo.ContentPackageNames.Count != serverInfo.ContentPackageHashes.Count || @@ -739,7 +754,7 @@ namespace Barotrauma.Steam if (!CheckWorkshopItemEnabled(existingItem)) { - if (!EnableWorkShopItem(existingItem, false, out string errorMsg)) + if (!EnableWorkShopItem(existingItem, out string errorMsg)) { DebugConsole.NewMessage(errorMsg, Color.Red); new GUIMessageBox( @@ -866,6 +881,10 @@ namespace Barotrauma.Steam DebugConsole.NewMessage("Published workshop item " + item?.Title + " successfully.", Microsoft.Xna.Framework.Color.LightGreen); contentPackage.SteamWorkshopUrl = $"http://steamcommunity.com/sharedfiles/filedetails/?source=Facepunch.Steamworks&id={task.Result.FileId.Value}"; + //NOTE: This sets InstallTime one hour into the future to guarantee + //that the published content package won't be autoupdated incorrectly. + //Change if it causes issues. + contentPackage.InstallTime = DateTime.UtcNow + TimeSpan.FromHours(1); contentPackage.Save(contentPackage.Path); SubscribeToWorkshopItem(task.Result.FileId); @@ -877,7 +896,7 @@ namespace Barotrauma.Steam /// /// Enables a workshop item by moving it to the game folder. /// - public static bool EnableWorkShopItem(Steamworks.Ugc.Item? item, bool allowFileOverwrite, out string errorMsg, bool selectContentPackage = false, bool suppressInstallNotif = false) + public static bool EnableWorkShopItem(Steamworks.Ugc.Item? item, out string errorMsg, bool selectContentPackage = false, bool suppressInstallNotif = false) { if (!(item?.IsInstalled ?? false)) { @@ -901,12 +920,21 @@ namespace Barotrauma.Steam }; string newContentPackagePath = GetWorkshopItemContentPackagePath(contentPackage); - if (ContentPackage.List.Any(cp => cp.Path.CleanUpPath() == newContentPackagePath.CleanUpPath())) + List existingPackages = ContentPackage.List.Where(cp => cp.Path.CleanUpPath() == newContentPackagePath.CleanUpPath()).ToList(); + if (existingPackages.Any()) { - errorMsg = TextManager.GetWithVariables("WorkshopErrorSamePathInstalled", - new string[] { "[itemname]", "[itempath]" }, - new string[] { item?.Title, Path.GetDirectoryName(newContentPackagePath) }); - return false; + if (item?.Owner.Id != Steamworks.SteamClient.SteamId) + { + errorMsg = TextManager.GetWithVariables("WorkshopErrorSamePathInstalled", + new string[] { "[itemname]", "[itempath]" }, + new string[] { item?.Title, Path.GetDirectoryName(newContentPackagePath) }); + return false; + } + else + { + RemoveMods(cp => !string.IsNullOrWhiteSpace(cp.SteamWorkshopUrl) && cp.SteamWorkshopUrl == contentPackage.SteamWorkshopUrl, + false); + } } if (!contentPackage.IsCompatible()) @@ -929,15 +957,9 @@ namespace Barotrauma.Steam { if (modCopiesInProgress.ContainsKey(item.Value.Id)) { - if (!modCopiesInProgress[item.Value.Id].IsCompleted && - !modCopiesInProgress[item.Value.Id].IsFaulted && - !modCopiesInProgress[item.Value.Id].IsCanceled) - { - errorMsg = ""; return true; - } - modCopiesInProgress.Remove(item.Value.Id); + errorMsg = ""; return true; } - newTask = CopyWorkShopItemAsync(item, contentPackage, newContentPackagePath, metaDataFilePath, allowFileOverwrite); + newTask = CopyWorkShopItemAsync(item, contentPackage, newContentPackagePath, metaDataFilePath); modCopiesInProgress.Add(item.Value.Id, newTask); } @@ -945,67 +967,85 @@ namespace Barotrauma.Steam contentPackage, (task, cp) => { - if (task.IsFaulted || task.IsCanceled) + try { - DebugConsole.ThrowError($"Failed to copy \"{item?.Title}\"", task.Exception); - GameMain.SteamWorkshopScreen?.SetReinstallButtonStatus(item, true, GUI.Style.Red); - return; - } - if (!string.IsNullOrWhiteSpace(task.Result)) - { - DebugConsole.ThrowError($"Failed to copy \"{item?.Title}\": {task.Result}"); - GameMain.SteamWorkshopScreen?.SetReinstallButtonStatus(item, true, GUI.Style.Red); - return; - } - - GameMain.Config.SuppressModFolderWatcher = true; - - var newPackage = new ContentPackage(cp.Path, newContentPackagePath) - { - SteamWorkshopUrl = item?.Url, - InstallTime = item?.Updated > item?.Created ? item?.Updated : item?.Created - }; - - foreach (ContentFile contentFile in newPackage.Files) - { - contentFile.Path = CorrectContentFilePath(contentFile.Path, cp, true); - } - - if (!Directory.Exists(Path.GetDirectoryName(newContentPackagePath))) - { - Directory.CreateDirectory(Path.GetDirectoryName(newContentPackagePath)); - } - newPackage.Save(newContentPackagePath); - ContentPackage.List.Add(newPackage); - - if (selectContentPackage) - { - if (newPackage.CorePackage) + if (task.IsFaulted || task.IsCanceled) { - GameMain.Config.SelectCorePackage(newPackage); + DebugConsole.ThrowError($"Failed to copy \"{item?.Title}\"", task.Exception); + GameMain.SteamWorkshopScreen?.SetReinstallButtonStatus(item, true, GUI.Style.Red); + return; } - else + if (!string.IsNullOrWhiteSpace(task.Result)) { - GameMain.Config.SelectContentPackage(newPackage); + DebugConsole.ThrowError($"Failed to copy \"{item?.Title}\": {task.Result}"); + GameMain.SteamWorkshopScreen?.SetReinstallButtonStatus(item, true, GUI.Style.Red); + return; } - GameMain.Config.SaveNewPlayerConfig(); - GameMain.Config.WarnIfContentPackageSelectionDirty(); + GameMain.Config.SuppressModFolderWatcher = true; - if (newPackage.Files.Any(f => f.Type == ContentType.Submarine)) + var newPackage = new ContentPackage(cp.Path, newContentPackagePath) { - SubmarineInfo.RefreshSavedSubs(); + SteamWorkshopUrl = item?.Url, + InstallTime = item?.Updated > item?.Created ? item?.Updated : item?.Created + }; + + foreach (ContentFile contentFile in newPackage.Files) + { + contentFile.Path = CorrectContentFilePath(contentFile.Path, contentFile.Type, cp, true); } + + foreach (ContentFile file in existingPackages.SelectMany(p => p.Files)) + { + string path = CorrectContentFilePath(file.Path, file.Type, cp, true).CleanUpPath(); + if (newPackage.Files.Any(f => f.Path.CleanUpPath() == path)) { continue; } + newPackage.AddFile(path, file.Type); + } + + if (!Directory.Exists(Path.GetDirectoryName(newContentPackagePath))) + { + Directory.CreateDirectory(Path.GetDirectoryName(newContentPackagePath)); + } + newPackage.Save(newContentPackagePath); + ContentPackage.List.Add(newPackage); + + if (selectContentPackage) + { + if (newPackage.CorePackage) + { + GameMain.Config.SelectCorePackage(newPackage); + } + else + { + GameMain.Config.SelectContentPackage(newPackage); + } + GameMain.Config.SaveNewPlayerConfig(); + + GameMain.Config.WarnIfContentPackageSelectionDirty(); + + if (newPackage.Files.Any(f => f.Type == ContentType.Submarine)) + { + SubmarineInfo.RefreshSavedSubs(); + } + } + else if (!suppressInstallNotif) + { + GameMain.MainMenuScreen?.SetEnableModsNotification(true); + } + + GameMain.Config.SuppressModFolderWatcher = false; + + GameMain.SteamWorkshopScreen?.SetReinstallButtonStatus(item, true, GUI.Style.Green); + } - else if (!suppressInstallNotif) + catch { - GameMain.MainMenuScreen?.SetEnableModsNotification(true); + throw; + } + finally + { + modCopiesInProgress.Remove(item.Value.Id); } - - GameMain.Config.SuppressModFolderWatcher = false; - - GameMain.SteamWorkshopScreen?.SetReinstallButtonStatus(item, true, GUI.Style.Green); - }); errorMsg = ""; @@ -1016,7 +1056,7 @@ namespace Barotrauma.Steam /// Asynchronously copies a Workshop item into the Mods folder. /// /// Returns an empty string on success, otherwise returns an error message. - private async static Task CopyWorkShopItemAsync(Steamworks.Ugc.Item? item, ContentPackage contentPackage, string newContentPackagePath, string metaDataFilePath, bool allowFileOverwrite) + private async static Task CopyWorkShopItemAsync(Steamworks.Ugc.Item? item, ContentPackage contentPackage, string newContentPackagePath, string metaDataFilePath) { await Task.Yield(); @@ -1029,13 +1069,13 @@ namespace Barotrauma.Steam Directory.CreateDirectory(targetPath); File.WriteAllText(copyingPath, "TEMPORARY FILE"); - SaveUtil.CopyFolder(item?.Directory, targetPath, copySubDirs: true, overwriteExisting: true); + SaveUtil.CopyFolder(item?.Directory, targetPath, copySubDirs: true, overwriteExisting: false); File.Delete(copyingPath); return ""; } - var allPackageFiles = Directory.GetFiles(item?.Directory, "*", SearchOption.AllDirectories); + var allPackageFiles = Directory.GetFiles(item?.Directory, "*", System.IO.SearchOption.AllDirectories); List nonContentFiles = new List(); foreach (string file in allPackageFiles) { @@ -1046,27 +1086,24 @@ namespace Barotrauma.Steam nonContentFiles.Add(relativePath); } - if (!allowFileOverwrite) + /*if (File.Exists(newContentPackagePath) && !CheckFileEquality(newContentPackagePath, metaDataFilePath)) { - if (File.Exists(newContentPackagePath) && !CheckFileEquality(newContentPackagePath, metaDataFilePath)) + errorMsg = TextManager.GetWithVariables("WorkshopErrorOverwriteOnEnable", new string[2] { "[itemname]", "[filename]" }, new string[2] { item?.Title, newContentPackagePath }); + DebugConsole.NewMessage(errorMsg, Color.Red); + return errorMsg; + } + + foreach (ContentFile contentFile in contentPackage.Files) + { + string sourceFile = Path.Combine(item?.Directory, contentFile.Path); + + if (File.Exists(sourceFile) && File.Exists(contentFile.Path) && !CheckFileEquality(sourceFile, contentFile.Path)) { - errorMsg = TextManager.GetWithVariables("WorkshopErrorOverwriteOnEnable", new string[2] { "[itemname]", "[filename]" }, new string[2] { item?.Title, newContentPackagePath }); + errorMsg = TextManager.GetWithVariables("WorkshopErrorOverwriteOnEnable", new string[2] { "[itemname]", "[filename]" }, new string[2] { item?.Title, contentFile.Path }); DebugConsole.NewMessage(errorMsg, Color.Red); return errorMsg; } - - foreach (ContentFile contentFile in contentPackage.Files) - { - string sourceFile = Path.Combine(item?.Directory, contentFile.Path); - - if (File.Exists(sourceFile) && File.Exists(contentFile.Path) && !CheckFileEquality(sourceFile, contentFile.Path)) - { - errorMsg = TextManager.GetWithVariables("WorkshopErrorOverwriteOnEnable", new string[2] { "[itemname]", "[filename]" }, new string[2] { item?.Title, contentFile.Path }); - DebugConsole.NewMessage(errorMsg, Color.Red); - return errorMsg; - } - } - } + }*/ Directory.CreateDirectory(targetPath); File.WriteAllText(copyingPath, "TEMPORARY FILE"); @@ -1084,7 +1121,7 @@ namespace Barotrauma.Steam } } - contentFile.Path = CorrectContentFilePath(contentFile.Path, contentPackage, + contentFile.Path = CorrectContentFilePath(contentFile.Path, contentFile.Type, contentPackage, contentFile.Type != ContentType.Submarine); //path not allowed -> the content file must be a reference to an external file (such as some vanilla file outside the Mods folder) @@ -1122,16 +1159,16 @@ namespace Barotrauma.Steam //make sure the destination directory exists Directory.CreateDirectory(Path.GetDirectoryName(contentFile.Path)); - CorrectContentFileCopy(contentPackage, sourceFile, contentFile.Path, overwrite: true); + CorrectContentFileCopy(contentPackage, sourceFile, contentFile.Path, overwrite: false); } foreach (string nonContentFile in nonContentFiles) { string sourceFile = Path.Combine(item?.Directory, nonContentFile); if (!File.Exists(sourceFile)) { continue; } - string destinationPath = CorrectContentFilePath(nonContentFile, contentPackage, false); + string destinationPath = CorrectContentFilePath(nonContentFile, ContentType.None, contentPackage, false); Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)); - CorrectContentFileCopy(contentPackage, sourceFile, destinationPath, overwrite: true); + CorrectContentFileCopy(contentPackage, sourceFile, destinationPath, overwrite: false); } File.Delete(copyingPath); @@ -1154,7 +1191,7 @@ namespace Barotrauma.Steam } } - private static void RemoveMods(Func predicate) + private static void RemoveMods(Func predicate, bool delete = true) { var toRemove = ContentPackage.List.Where(predicate).ToList(); var packagesToDeselect = GameMain.Config.SelectedContentPackages.Where(p => toRemove.Contains(p)).ToList(); @@ -1170,16 +1207,19 @@ namespace Barotrauma.Steam } } - foreach (var cp in toRemove) + if (delete) { - try + foreach (var cp in toRemove) { - string path = Path.GetDirectoryName(cp.Path); - if (Directory.Exists(path)) { Directory.Delete(path, true); } - } - catch (Exception e) - { - DebugConsole.ThrowError($"An error occurred while attempting to delete {Path.GetDirectoryName(cp.Path)}", e); + try + { + string path = Path.GetDirectoryName(cp.Path); + if (Directory.Exists(path)) { Directory.Delete(path, true); } + } + catch (Exception e) + { + DebugConsole.ThrowError($"An error occurred while attempting to delete {Path.GetDirectoryName(cp.Path)}", e); + } } } @@ -1245,7 +1285,7 @@ namespace Barotrauma.Steam string metaDataPath = Path.Combine(item?.Directory, MetadataFileName); if (!File.Exists(metaDataPath)) { - throw new FileNotFoundException("Metadata file for the Workshop item \"" + item?.Title + "\" not found. The file may be corrupted."); + throw new System.IO.FileNotFoundException("Metadata file for the Workshop item \"" + item?.Title + "\" not found. The file may be corrupted."); } ContentPackage contentPackage = new ContentPackage(metaDataPath); @@ -1268,7 +1308,7 @@ namespace Barotrauma.Steam { metaDataPath = Path.Combine(item?.Directory, MetadataFileName); } - catch (ArgumentException e) + catch (ArgumentException) { string errorMessage = "Metadata file for the Workshop item \"" + item?.Title + "\" not found. Could not combine path (" + (item?.Directory ?? "directory name empty") + ")."; @@ -1328,6 +1368,8 @@ namespace Barotrauma.Steam public static async Task AutoUpdateWorkshopItemsAsync() { + await Task.Yield(); + if (!isInitialized) { return false; } var query = new Steamworks.Ugc.Query(Steamworks.UgcType.All) @@ -1343,6 +1385,8 @@ namespace Barotrauma.Steam GameMain.Config.SuppressModFolderWatcher = false; + + List updateNotifications = new List(); foreach (var item in items) { try @@ -1353,7 +1397,7 @@ namespace Barotrauma.Steam string errorMsg; if (!CheckWorkshopItemEnabled(item)) { - installedSuccessfully = EnableWorkShopItem(item, true, out errorMsg); + installedSuccessfully = EnableWorkShopItem(item, out errorMsg); } else if (!CheckWorkshopItemUpToDate(item)) { @@ -1366,29 +1410,65 @@ namespace Barotrauma.Steam if (!installedSuccessfully) { - DebugConsole.ThrowError(errorMsg); - new GUIMessageBox( - TextManager.Get("Error"), - TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { item.Title, errorMsg })); + CrossThread.RequestExecutionOnMainThread(() => + { + DebugConsole.NewMessage(errorMsg, Color.Red); + string errorId = errorMsg; + if (!GUIMessageBox.MessageBoxes.Any(m => m.UserData as string == errorId)) + { + new GUIMessageBox( + TextManager.Get("Error"), + TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { item.Title, errorMsg })) + { + UserData = errorId + }; + } + }); } else { - //TODO: potential race condition - new GUIMessageBox("", TextManager.GetWithVariable("WorkshopItemUpdated", "[itemname]", item.Title)); + updateNotifications.Add(TextManager.GetWithVariable("WorkshopItemUpdated", "[itemname]", item.Title)); } } catch (Exception e) { - new GUIMessageBox( - TextManager.Get("Error"), - TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { item.Title, e.Message + ", " + e.TargetSite })); - GameAnalyticsManager.AddErrorEventOnce( - "SteamManager.AutoUpdateWorkshopItems:" + e.Message, - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Failed to autoupdate workshop item \"" + item.Title + "\". " + e.Message + "\n" + e.StackTrace); + CrossThread.RequestExecutionOnMainThread(() => + { + string errorId = e.Message; + if (!GUIMessageBox.MessageBoxes.Any(m => m.UserData as string == errorId)) + { + new GUIMessageBox( + TextManager.Get("Error"), + TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { item.Title, e.Message + ", " + e.TargetSite })) + { + UserData = errorId + }; + } + GameAnalyticsManager.AddErrorEventOnce( + "SteamManager.AutoUpdateWorkshopItems:" + e.Message, + GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + "Failed to autoupdate workshop item \"" + item.Title + "\". " + e.Message + "\n" + e.StackTrace); + }); } } + if (updateNotifications.Count > 0) + { + CrossThread.RequestExecutionOnMainThread(() => + { + while (updateNotifications.Count > 0) + { + float width = updateNotifications.Max(notif => GUI.Font.MeasureString(notif).X) * 1.25f; + + int notificationsPerMsgBox = 20; + new GUIMessageBox("", string.Join('\n', updateNotifications.Take(notificationsPerMsgBox)), + relativeSize: new Microsoft.Xna.Framework.Vector2(0.25f, 0.0f), + minSize: new Microsoft.Xna.Framework.Point((int)width, 0)); + updateNotifications.RemoveRange(0, Math.Min(notificationsPerMsgBox, updateNotifications.Count)); + } + }); + } + List tasks; lock (modCopiesInProgress) { @@ -1403,16 +1483,20 @@ namespace Barotrauma.Steam { errorMsg = ""; if (!(item?.IsInstalled ?? false)) { return false; } - if (!DisableWorkShopItem(item, false, out errorMsg)) { return false; } - if (!EnableWorkShopItem(item, allowFileOverwrite: false, errorMsg: out errorMsg)) { return false; } - + bool reenable = GameMain.Config.SelectedContentPackages.Any(p => !string.IsNullOrEmpty(p.SteamWorkshopUrl) && GetWorkshopItemIDFromUrl(p.SteamWorkshopUrl) == item?.Id); + if (item?.Owner.Id != Steamworks.SteamClient.SteamId) + { + if (!DisableWorkShopItem(item, false, out errorMsg)) { return false; } + } + if (!EnableWorkShopItem(item, errorMsg: out errorMsg, selectContentPackage: reenable)) { return false; } return true; } private static string GetWorkshopItemContentPackagePath(ContentPackage contentPackage) { - string packageName = contentPackage.Name; + string packageName = contentPackage.Name.Trim(); packageName = ToolBox.RemoveInvalidFileNameChars(packageName); + while (packageName.Last() == '.') { packageName = packageName.Substring(0, packageName.Length-1); } //packageName = packageName + "_" + GetWorkshopItemIDFromUrl(contentPackage.SteamWorkshopUrl); return Path.Combine("Mods", packageName, MetadataFileName); @@ -1429,7 +1513,9 @@ namespace Barotrauma.Steam attr.Name.ToString() == "characterfile") && attr.Value.CleanUpPath().Contains("/")) { - attr.Value = CorrectContentFilePath(attr.Value, package, true); + ContentType type = ContentType.None; + Enum.TryParse(attr.Name.LocalName, true, out type); + attr.Value = CorrectContentFilePath(attr.Value, type, package, true); } } @@ -1441,18 +1527,20 @@ namespace Barotrauma.Steam private static void CorrectContentFileCopy(ContentPackage package, string src, string dest, bool overwrite) { + if (!overwrite && File.Exists(dest)) { return; } + if (Path.GetExtension(src).Equals(".xml", StringComparison.OrdinalIgnoreCase)) { XDocument doc = XMLExtensions.TryLoadXml(src); if (doc != null) { CorrectXMLFilePaths(package, doc.Root); - using (MemoryStream stream = new MemoryStream()) + using (System.IO.MemoryStream stream = new System.IO.MemoryStream()) { - XmlWriterSettings settings = new XmlWriterSettings(); + System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings(); settings.Indent = true; settings.Encoding = new System.Text.UTF8Encoding(false); - using (var xmlWriter = XmlWriter.Create(stream, settings)) + using (var xmlWriter = System.Xml.XmlWriter.Create(stream, settings)) { doc.WriteTo(xmlWriter); xmlWriter.Flush(); @@ -1463,23 +1551,29 @@ namespace Barotrauma.Steam } else { - File.Copy(src, dest, overwrite: overwrite); + File.Copy(src, dest, overwrite: true); } } else { - File.Copy(src, dest, overwrite: overwrite); + File.Copy(src, dest, overwrite: true); } } - private static string CorrectContentFilePath(string contentFilePath, ContentPackage package, bool checkIfFileExists = false) + private static string CorrectContentFilePath(string contentFilePath, ContentType type, ContentPackage package, bool checkIfFileExists = false) { string packageName = Path.GetDirectoryName(GetWorkshopItemContentPackagePath(package)); contentFilePath = contentFilePath.CleanUpPathCrossPlatform(); - if (checkIfFileExists && File.Exists(contentFilePath)) + if (checkIfFileExists) { + bool exists = File.Exists(contentFilePath); + if (type == ContentType.Executable || + type == ContentType.ServerExecutable) + { + exists |= File.Exists(contentFilePath + ".dll"); + } return contentFilePath; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs index bf506cd08..c73bde30c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs @@ -149,9 +149,15 @@ namespace Barotrauma.Networking } } + short[] uncompressedBuffer = new short[VoipConfig.BUFFER_SIZE]; + short[] prevUncompressedBuffer = new short[VoipConfig.BUFFER_SIZE]; + bool prevCaptured = true; + int captureTimer; + void UpdateCapture() { - short[] uncompressedBuffer = new short[VoipConfig.BUFFER_SIZE]; + Array.Copy(uncompressedBuffer, 0, prevUncompressedBuffer, 0, VoipConfig.BUFFER_SIZE); + Array.Clear(uncompressedBuffer, 0, VoipConfig.BUFFER_SIZE); while (capturing) { int alcError; @@ -202,6 +208,21 @@ namespace Barotrauma.Networking bool allowEnqueue = false; if (GameMain.WindowActive) { + ForceLocal = captureTimer > 0 ? ForceLocal : false; + bool pttDown = false; + if ((PlayerInput.KeyDown(InputType.Voice) || PlayerInput.KeyDown(InputType.LocalVoice)) && + GUI.KeyboardDispatcher.Subscriber == null) + { + pttDown = true; + if (PlayerInput.KeyDown(InputType.LocalVoice)) + { + ForceLocal = true; + } + else + { + ForceLocal = false; + } + } if (GameMain.Config.VoiceSetting == GameSettings.VoiceMode.Activity) { if (dB > GameMain.Config.NoiseGateThreshold) @@ -211,25 +232,38 @@ namespace Barotrauma.Networking } else if (GameMain.Config.VoiceSetting == GameSettings.VoiceMode.PushToTalk) { - if (PlayerInput.KeyDown(InputType.Voice) && GUI.KeyboardDispatcher.Subscriber == null) + if (pttDown) { allowEnqueue = true; } } } - if (allowEnqueue) + if (allowEnqueue || captureTimer > 0) { LastEnqueueAudio = DateTime.Now; //encode audio and enqueue it lock (buffers) { + if (!prevCaptured) //enqueue the previous buffer if not sent to avoid cutoff + { + int compressedCountPrev = VoipConfig.Encoder.Encode(prevUncompressedBuffer, 0, VoipConfig.BUFFER_SIZE, BufferToQueue, 0, VoipConfig.MAX_COMPRESSED_SIZE); + EnqueueBuffer(compressedCountPrev); + } int compressedCount = VoipConfig.Encoder.Encode(uncompressedBuffer, 0, VoipConfig.BUFFER_SIZE, BufferToQueue, 0, VoipConfig.MAX_COMPRESSED_SIZE); EnqueueBuffer(compressedCount); } + captureTimer -= (VoipConfig.BUFFER_SIZE * 1000) / VoipConfig.FREQUENCY; + if (allowEnqueue) + { + captureTimer = GameMain.Config.VoiceChatCutoffPrevention; + } + prevCaptured = true; } else { + captureTimer = 0; + prevCaptured = false; //enqueue silence lock (buffers) { @@ -241,14 +275,6 @@ namespace Barotrauma.Networking } } - public override void Write(IWriteMessage msg) - { - lock (buffers) - { - base.Write(msg); - } - } - public override void Dispose() { Instance = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs index 574dffcb2..b648cde9c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs @@ -85,11 +85,10 @@ namespace Barotrauma.Networking return; } - if (queue.Read(msg)) + Client client = gameClient.ConnectedClients.Find(c => c.VoipQueue == queue); + if (queue.Read(msg, discardData: client.Muted || client.MutedLocally)) { - Client client = gameClient.ConnectedClients.Find(c => c.VoipQueue == queue); if (client.Muted || client.MutedLocally) { return; } - if (client.VoipSound == null) { DebugConsole.Log("Recreating voipsound " + queueId); @@ -98,7 +97,8 @@ namespace Barotrauma.Networking if (client.Character != null && !client.Character.IsDead && !client.Character.Removed && client.Character.SpeechImpediment <= 100.0f) { - var messageType = ChatMessage.CanUseRadio(client.Character, out WifiComponent radio) ? ChatMessageType.Radio : ChatMessageType.Default; + WifiComponent radio = null; + var messageType = !client.VoipQueue.ForceLocal && ChatMessage.CanUseRadio(client.Character, out radio) ? ChatMessageType.Radio : ChatMessageType.Default; client.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); client.VoipSound.UseRadioFilter = messageType == ChatMessageType.Radio; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs index 49650e599..026f4f287 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs @@ -61,10 +61,10 @@ namespace Barotrauma foreach (GUIComponent comp in listBox.Content.Children) { - if (comp.FindChild("votes") is GUITextBlock voteText) comp.RemoveChild(voteText); + if (comp.FindChild("votes") is GUITextBlock voteText) { comp.RemoveChild(voteText); } } - if (clients == null) return; + if (clients == null) { return; } List> voteList = GetVoteList(voteType, clients); foreach (Pair votable in voteList) @@ -73,7 +73,7 @@ namespace Barotrauma } break; case VoteType.StartRound: - if (clients == null) return; + if (clients == null) { return; } foreach (Client client in clients) { var clientReady = GameMain.NetLobbyScreen.PlayerList.Content.FindChild(client)?.FindChild("clientready"); @@ -113,18 +113,18 @@ namespace Barotrauma switch (voteType) { case VoteType.Sub: - Submarine sub = data as Submarine; - if (sub == null) return; + SubmarineInfo sub = data as SubmarineInfo; + if (sub == null) { return; } - msg.Write(sub.Info.Name); + msg.Write(sub.Name); break; case VoteType.Mode: GameModePreset gameMode = data as GameModePreset; - if (gameMode == null) return; + if (gameMode == null) { return; } msg.Write(gameMode.Identifier); break; case VoteType.EndRound: - if (!(data is bool)) return; + if (!(data is bool)) { return; } msg.Write((bool)data); break; case VoteType.Kick: @@ -153,12 +153,12 @@ namespace Barotrauma { int votes = inc.ReadByte(); string subName = inc.ReadString(); - List serversubs = new List(); + List serversubs = new List(); foreach (GUIComponent item in GameMain.NetLobbyScreen?.SubList?.Content?.Children) { - if (item.UserData != null && item.UserData is Submarine) serversubs.Add(item.UserData as Submarine); + if (item.UserData != null && item.UserData is SubmarineInfo) { serversubs.Add(item.UserData as SubmarineInfo); } } - Submarine sub = serversubs.FirstOrDefault(sm => sm.Info.Name == subName); + SubmarineInfo sub = serversubs.FirstOrDefault(s => s.Name == subName); SetVoteText(GameMain.NetLobbyScreen.SubList, sub, votes); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs index f2ee5ffa5..c1f64c6cd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/Particle.cs @@ -10,6 +10,8 @@ namespace Barotrauma.Particles { private ParticlePrefab prefab; + private string debugName = "Particle (uninitialized)"; + public delegate void OnChangeHullHandler(Vector2 position, Hull currentHull); public OnChangeHullHandler OnChangeHull; @@ -92,10 +94,16 @@ namespace Barotrauma.Particles { get { return prefab; } } - + + public override string ToString() + { + return debugName; + } + public void Init(ParticlePrefab prefab, Vector2 position, Vector2 speed, float rotation, Hull hullGuess = null, bool drawOnTop = false) { this.prefab = prefab; + debugName = $"Particle ({prefab.Name})"; spriteIndex = Rand.Int(prefab.Sprites.Count); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs index 6a140118c..ab81a8534 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs @@ -89,7 +89,21 @@ namespace Barotrauma.Particles { public readonly string Name; - public readonly ParticlePrefab ParticlePrefab; + private string particlePrefabName; + + private ParticlePrefab particlePrefab; + public ParticlePrefab ParticlePrefab + { + get + { + if (particlePrefab == null && particlePrefabName != null) + { + particlePrefab = GameMain.ParticleManager?.FindPrefab(particlePrefabName); + if (particlePrefab == null) { particlePrefabName = null; } + } + return particlePrefab; + } + } public readonly float AngleMin, AngleMax; @@ -114,8 +128,7 @@ namespace Barotrauma.Particles public ParticleEmitterPrefab(XElement element) { Name = element.Name.ToString(); - - ParticlePrefab = GameMain.ParticleManager.FindPrefab(element.GetAttributeString("particle", "")); + particlePrefabName = element.GetAttributeString("particle", ""); if (element.Attribute("startrotation") == null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs index c362ef365..9d444cd03 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Physics/PhysicsBody.cs @@ -17,7 +17,7 @@ namespace Barotrauma get { return bodyShapeTexture; } } - public void Draw(DeformableSprite deformSprite, Camera cam, Vector2 scale, Color color, bool mirror = false) + public void Draw(DeformableSprite deformSprite, Camera cam, Vector2 scale, Color color, bool invert = false) { if (!Enabled) return; UpdateDrawPosition(); @@ -25,7 +25,7 @@ namespace Barotrauma new Vector3(DrawPosition, MathHelper.Clamp(deformSprite.Sprite.Depth, 0, 1)), deformSprite.Origin, -DrawRotation, - scale, color, Dir < 0, mirror); + scale, color, Dir < 0, invert); } public void Draw(SpriteBatch spriteBatch, Sprite sprite, Color color, float? depth = null, float scale = 1.0f, bool mirrorX = false, bool mirrorY = false) diff --git a/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs b/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs index 5e2f6e3bb..3b9293bef 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs @@ -25,6 +25,18 @@ namespace Barotrauma public class KeyOrMouse { public Keys Key { get; private set; } + + private string name; + + public string Name + { + get + { + if (name == null) { name = GetName(); } + return name; + } + } + public MouseButton MouseButton { get; private set; } public KeyOrMouse(Keys keyBinding) @@ -133,6 +145,30 @@ namespace Barotrauma hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode((int)MouseButton); return hashCode; } + + public string GetName() + { + if (PlayerInput.NumberKeys.Contains(Key)) + { + return Key.ToString().Substring(1, 1); + } + if (MouseButton != MouseButton.None) + { + switch (MouseButton) + { + case MouseButton.PrimaryMouse: + return PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.rightmouse") : TextManager.Get("input.leftmouse"); + case MouseButton.SecondaryMouse: + return PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.leftmouse") : TextManager.Get("input.rightmouse"); + default: + return TextManager.Get("input." + MouseButton.ToString().ToLowerInvariant()); + } + } + else + { + return Key.ToString(); + } + } } public static class PlayerInput @@ -155,6 +191,8 @@ namespace Barotrauma static bool allowInput; static bool wasWindowActive; + public static readonly List NumberKeys = new List { Keys.D0, Keys.D1, Keys.D2, Keys.D3, Keys.D4, Keys.D5, Keys.D6, Keys.D7, Keys.D8, Keys.D9 }; + #if WINDOWS [DllImport("user32.dll")] static extern int GetSystemMetrics(int smIndex); @@ -408,6 +446,12 @@ namespace Barotrauma return (AllowInput && oldKeyboardState.IsKeyDown(button) && keyboardState.IsKeyUp(button)); } + public static bool InventoryKeyHit(int index) + { + if (index == -1) return false; + return AllowInput && GameMain.Config.InventoryKeyBind(index).IsHit(); + } + public static bool KeyDown(Keys button) { return (AllowInput && keyboardState.IsKeyDown(button)); @@ -418,6 +462,16 @@ namespace Barotrauma return AllowInput && keyboardState.IsKeyUp(button); } + public static bool IsShiftDown() + { + return KeyDown(Keys.LeftShift) || KeyDown(Keys.RightShift); + } + + public static bool IsCtrlDown() + { + return KeyDown(Keys.LeftControl) || KeyDown(Keys.RightControl); + } + public static void Update(double deltaTime) { timeSinceClick += deltaTime; @@ -448,16 +502,30 @@ namespace Barotrauma MouseSpeedPerSecond = MouseSpeed / (float)deltaTime; + // Split into two to not accept drag & drop releasing as part of a double-click doubleClicked = false; if (PrimaryMouseButtonClicked()) { - if (timeSinceClick < DoubleClickDelay && - (mouseState.Position - lastClickPosition).ToVector2().Length() < MaxDoubleClickDistance) + float dist = (mouseState.Position - lastClickPosition).ToVector2().Length(); + + if (timeSinceClick < DoubleClickDelay && dist < MaxDoubleClickDistance) { doubleClicked = true; + timeSinceClick = DoubleClickDelay; } - lastClickPosition = mouseState.Position; - timeSinceClick = 0.0; + else if (timeSinceClick < DoubleClickDelay) + { + lastClickPosition = mouseState.Position; + } + if (!doubleClicked && dist < MaxDoubleClickDistance) + { + timeSinceClick = 0.0; + } + } + + if (PrimaryMouseButtonDown()) + { + lastClickPosition = mouseState.Position; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Program.cs b/Barotrauma/BarotraumaClient/ClientSource/Program.cs index 6ecf9226b..e928d9dc1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Program.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Program.cs @@ -1,7 +1,7 @@ #region Using Statements using System; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Text; using GameAnalyticsSDK.Net; @@ -104,8 +104,6 @@ namespace Barotrauma exeHash = new Md5Hash(stream); } - StreamWriter sw = new StreamWriter(filePath); - StringBuilder sb = new StringBuilder(); sb.AppendLine("Barotrauma Client crash report (generated on " + DateTime.Now + ")"); sb.AppendLine("\n"); @@ -148,7 +146,8 @@ namespace Barotrauma if (GameMain.Config != null) { sb.AppendLine("Graphics mode: " + GameMain.Config.GraphicsWidth + "x" + GameMain.Config.GraphicsHeight + " (" + GameMain.Config.WindowMode.ToString() + ")"); - sb.AppendLine("VSync "+ (GameMain.Config.VSyncEnabled ? "ON" : "OFF")); + sb.AppendLine("VSync " + (GameMain.Config.VSyncEnabled ? "ON" : "OFF")); + sb.AppendLine("Language: " + (GameMain.Config.Language ?? "none")); } if (GameMain.SelectedPackages != null) { @@ -234,8 +233,7 @@ namespace Barotrauma string crashReport = sb.ToString(); - sw.WriteLine(crashReport); - sw.Close(); + File.WriteAllText(filePath, crashReport); if (GameSettings.SaveDebugConsoleLogs) DebugConsole.SaveLogs(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs index bd4ae4c0c..776304554 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs @@ -2,7 +2,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; @@ -81,6 +81,7 @@ namespace Barotrauma var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUI.Font); var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform, Anchor.CenterRight), font: GUI.Font, createClearButton: true); + filterContainer.RectTransform.MinSize = searchBox.RectTransform.MinSize; searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; searchBox.OnTextChanged += (textBox, text) => { FilterSubs(subList, text); return true; }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index e05f157a0..ab4b6ac92 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -435,8 +435,8 @@ namespace Barotrauma { OnClicked = (btn, userdata) => { - if (GameMain.GameSession?.Submarine != null && - GameMain.GameSession.Submarine.LeftBehindSubDockingPortOccupied) + if (GameMain.GameSession?.SubmarineInfo != null && + GameMain.GameSession.SubmarineInfo.LeftBehindSubDockingPortOccupied) { new GUIMessageBox("", TextManager.Get("ReplaceShuttleDockingPortOccupied")); return true; @@ -917,7 +917,7 @@ namespace Barotrauma GUINumberInput.NumberType.Int) { MinValueInt = 0, - MaxValueInt = 100, + MaxValueInt = CargoManager.MaxQuantity, UserData = pi, IntValue = pi.Quantity }; @@ -927,7 +927,11 @@ namespace Barotrauma { if (suppressBuySell) { return; } PurchasedItem purchasedItem = numberInput.UserData as PurchasedItem; - + if (GameMain.Client != null && !GameMain.Client.HasPermission(Networking.ClientPermissions.ManageCampaign)) + { + numberInput.IntValue = purchasedItem.Quantity; + return; + } //Attempting to buy if (numberInput.IntValue > purchasedItem.Quantity) { @@ -965,15 +969,18 @@ namespace Barotrauma private bool BuyItem(GUIComponent component, object obj) { - if (!(obj is PurchasedItem pi) || pi.ItemPrefab == null) return false; + if (!(obj is PurchasedItem pi) || pi.ItemPrefab == null) { return false; } if (GameMain.Client != null && !GameMain.Client.HasPermission(Networking.ClientPermissions.ManageCampaign)) { return false; } - + + var purchasedItem = Campaign.CargoManager.PurchasedItems.Find(pi2 => pi2.ItemPrefab == pi.ItemPrefab); + if (purchasedItem != null && purchasedItem.Quantity >= CargoManager.MaxQuantity) { return false; } + PriceInfo priceInfo = pi.ItemPrefab.GetPrice(Campaign.Map.CurrentLocation); - if (priceInfo == null || priceInfo.BuyPrice > Campaign.Money) return false; + if (priceInfo == null || priceInfo.BuyPrice > Campaign.Money) { return false; } Campaign.CargoManager.PurchaseItem(pi.ItemPrefab, 1); GameMain.Client?.SendCampaignState(); @@ -983,7 +990,7 @@ namespace Barotrauma private bool SellItem(GUIComponent component, object obj) { - if (!(obj is PurchasedItem pi) || pi.ItemPrefab == null) return false; + if (!(obj is PurchasedItem pi) || pi.ItemPrefab == null) { return false; } if (GameMain.Client != null && !GameMain.Client.HasPermission(Networking.ClientPermissions.ManageCampaign)) { @@ -1068,7 +1075,7 @@ namespace Barotrauma (GameMain.Client == null || GameMain.Client.HasPermission(Networking.ClientPermissions.ManageCampaign)); repairItemsButton.GetChild().Selected = Campaign.PurchasedItemRepairs; - if (GameMain.GameSession?.Submarine == null || !GameMain.GameSession.Submarine.SubsLeftBehind) + if (GameMain.GameSession?.SubmarineInfo == null || !GameMain.GameSession.SubmarineInfo.SubsLeftBehind) { replaceShuttlesButton.Enabled = false; replaceShuttlesButton.GetChild().Selected = false; @@ -1166,7 +1173,7 @@ namespace Barotrauma }; var characterPreviewContent = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.8f), characterPreviewFrame.RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.02f) }, style: null); - characterInfo.CreateInfoFrame(characterPreviewContent); + characterInfo.CreateInfoFrame(characterPreviewContent, true); } var currentCrew = GameMain.GameSession.CrewManager.GetCharacterInfos(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index e7ef26c18..b2af0381d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -2,13 +2,17 @@ using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Graphics; using System; -using System.IO; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using Barotrauma.Extensions; using FarseerPhysics; using FarseerPhysics.Dynamics; +#if DEBUG +using System.IO; +#else +using Barotrauma.IO; +#endif namespace Barotrauma.CharacterEditor { @@ -121,7 +125,10 @@ namespace Barotrauma.CharacterEditor ResetVariables(); var subInfo = new SubmarineInfo("Content/AnimEditor.sub"); Submarine.MainSub = new Submarine(subInfo); - Submarine.MainSub.PhysicsBody.Enabled = false; + if (Submarine.MainSub.PhysicsBody != null) + { + Submarine.MainSub.PhysicsBody.Enabled = false; + } originalWall = new WallGroup(new List(Structure.WallList)); CloneWalls(); CalculateMovementLimits(); @@ -275,7 +282,7 @@ namespace Barotrauma.CharacterEditor return TextManager.Get(screenTextTag + tag); } - #region Main methods +#region Main methods public override void AddToGUIUpdateList() { rightArea.AddToGUIUpdateList(); @@ -476,9 +483,16 @@ namespace Barotrauma.CharacterEditor if (character.IsHumanoid) { animTestPoseToggle.Enabled = CurrentAnimation.IsGroundedAnimation; - if (animTestPoseToggle.Enabled && PlayerInput.KeyHit(Keys.X)) + if (animTestPoseToggle.Enabled) { - SetToggle(animTestPoseToggle, !animTestPoseToggle.Selected); + if (PlayerInput.KeyHit(Keys.X)) + { + SetToggle(animTestPoseToggle, !animTestPoseToggle.Selected); + } + } + else + { + animTestPoseToggle.Selected = false; } } if (PlayerInput.KeyHit(InputType.Run)) @@ -652,9 +666,6 @@ namespace Barotrauma.CharacterEditor } if (!isFrozen) { - Submarine.MainSub.SetPrevTransform(Submarine.MainSub.Position); - Submarine.MainSub.Update((float)deltaTime); - foreach (PhysicsBody body in PhysicsBody.List) { body.SetPrevTransform(body.SimPosition, body.Rotation); @@ -981,9 +992,9 @@ namespace Barotrauma.CharacterEditor } spriteBatch.End(); } - #endregion +#endregion - #region Ragdoll Manipulation +#region Ragdoll Manipulation private void UpdateJointCreation() { if (jointCreationMode == JointCreationMode.None) @@ -1310,9 +1321,9 @@ namespace Barotrauma.CharacterEditor } RecreateRagdoll(); } - #endregion +#endregion - #region Endless runner +#region Endless runner private int min; private int max; private void CalculateMovementLimits() @@ -1415,9 +1426,9 @@ namespace Barotrauma.CharacterEditor AllWalls.ForEach(w => w.SetCollisionCategory(collisionCategory)); GameMain.World.ProcessChanges(); } - #endregion +#endregion - #region Character spawning +#region Character spawning private int characterIndex = -1; private string currentCharacterConfig; private string selectedJob = null; @@ -1739,7 +1750,11 @@ namespace Barotrauma.CharacterEditor { Directory.CreateDirectory(mainFolder); } +#if DEBUG doc.Save(configFilePath); +#else + doc.SaveSafe(configFilePath); +#endif // Add to the selected content package contentPackage.AddFile(configFilePath, ContentType.Character); contentPackage.Save(contentPackage.Path); @@ -1820,9 +1835,9 @@ namespace Barotrauma.CharacterEditor { character.Inventory?.Items.ForEachMod(i => i?.Unequip(character)); } - #endregion +#endregion - #region GUI +#region GUI private static Vector2 innerScale = new Vector2(0.95f, 0.95f); private GUILayoutGroup rightArea, leftArea; @@ -2117,7 +2132,7 @@ namespace Barotrauma.CharacterEditor CreateTextures(); return true; }; - new GUIButton(new RectTransform(buttonSize, parent.RectTransform, Anchor.BottomCenter), GetCharacterEditorTranslation("RecreateRagdoll")) + var recreateButton = new GUIButton(new RectTransform(buttonSize, parent.RectTransform, Anchor.BottomCenter), GetCharacterEditorTranslation("RecreateRagdoll")) { ToolTip = GetCharacterEditorTranslation("RecreateRagdollTooltip"), OnClicked = (button, data) => @@ -2127,6 +2142,7 @@ namespace Barotrauma.CharacterEditor return true; } }; + GUITextBlock.AutoScaleAndNormalize(reloadTexturesButton.TextBlock, recreateButton.TextBlock); buttonsPanelToggle = new ToggleButton(new RectTransform(new Vector2(0.08f, 1), buttonsPanel.RectTransform, Anchor.CenterRight, Pivot.CenterLeft), Direction.Left); buttonsPanel.RectTransform.MinSize = new Point(0, (int)(parent.RectTransform.Children.Sum(c => c.MinSize.Y) * 1.5f)); } @@ -3117,6 +3133,8 @@ namespace Barotrauma.CharacterEditor } }; + GUITextBlock.AutoScaleAndNormalize(layoutGroup.Children.Where(c => c is GUIButton).Select(c => ((GUIButton)c).TextBlock)); + fileEditToggle = new ToggleButton(new RectTransform(new Vector2(0.08f, 1), fileEditPanel.RectTransform, Anchor.CenterLeft, Pivot.CenterRight), Direction.Right); void ResetView() @@ -3134,9 +3152,9 @@ namespace Barotrauma.CharacterEditor fileEditPanel.RectTransform.MinSize = new Point(0, (int)(layoutGroup.RectTransform.Children.Sum(c => c.MinSize.Y + layoutGroup.AbsoluteSpacing) * 1.2f)); } - #endregion +#endregion - #region ToggleButtons +#region ToggleButtons private enum Direction { Left, @@ -3198,9 +3216,9 @@ namespace Barotrauma.CharacterEditor } } - #endregion +#endregion - #region Params +#region Params private CharacterParams CharacterParams => character.Params; private List AnimParams => character.AnimController.AllAnimParams; private AnimationParams CurrentAnimation => character.AnimController.CurrentAnimationParams; @@ -3451,9 +3469,9 @@ namespace Barotrauma.CharacterEditor } } } - #endregion +#endregion - #region Helpers +#region Helpers private Vector2 ScreenToSim(float x, float y) => ScreenToSim(new Vector2(x, y)); private Vector2 ScreenToSim(Vector2 p) => ConvertUnits.ToSimUnits(Cam.ScreenToWorld(p)) + Submarine.MainSub.SimPosition; private Vector2 SimToScreen(float x, float y) => SimToScreen(new Vector2(x, y)); @@ -3694,9 +3712,9 @@ namespace Barotrauma.CharacterEditor SetToggle(spritesheetToggle, true); } } - #endregion +#endregion - #region Animation Controls +#region Animation Controls private void DrawAnimationControls(SpriteBatch spriteBatch, float deltaTime) { var collider = character.AnimController.Collider; @@ -4289,9 +4307,9 @@ namespace Barotrauma.CharacterEditor } } } - #endregion +#endregion - #region Ragdoll +#region Ragdoll private Vector2[] corners = new Vector2[4]; private Vector2[] GetLimbPhysicRect(Limb limb) { @@ -4613,9 +4631,9 @@ namespace Barotrauma.CharacterEditor } return otherLimbs; } - #endregion +#endregion - #region Spritesheet +#region Spritesheet private List textures; private List Textures { @@ -5210,9 +5228,9 @@ namespace Barotrauma.CharacterEditor CalculateSpritesheetZoom(); spriteSheetZoomBar.BarScroll = MathHelper.Lerp(0, 1, MathUtils.InverseLerp(spriteSheetMinZoom, spriteSheetMaxZoom, spriteSheetZoom)); } - #endregion +#endregion - #region Widgets as methods +#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, int rounding = 1) { @@ -5321,9 +5339,9 @@ namespace Barotrauma.CharacterEditor } } } - #endregion +#endregion - #region Widgets as classes +#region Widgets as classes private Dictionary animationWidgets = new Dictionary(); private Dictionary jointSelectionWidgets = new Dictionary(); private Dictionary limbEditWidgets = new Dictionary(); @@ -5477,6 +5495,6 @@ namespace Barotrauma.CharacterEditor return w; } } - #endregion +#endregion } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs index 8f6eb15ce..cdb039970 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs @@ -1,6 +1,6 @@ using Microsoft.Xna.Framework; using System; -using System.IO; +using Barotrauma.IO; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index 8278e964c..d3c8f91f5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -17,7 +17,7 @@ namespace Barotrauma private RenderTarget2D renderTargetFinal; private Effect damageEffect; - private Texture2D damageStencil; + private Texture2D damageStencil; private Texture2D distortTexture; public Effect PostProcessEffect { get; private set; } @@ -151,7 +151,10 @@ namespace Barotrauma GameMain.ParticleManager.UpdateTransforms(); - GameMain.LightManager.ObstructVision = Character.Controlled != null && Character.Controlled.ObstructVision; + GameMain.LightManager.ObstructVision = + Character.Controlled != null && + Character.Controlled.ObstructVision && + (Character.Controlled.ViewTarget == Character.Controlled || Character.Controlled.ViewTarget == null); if (Character.Controlled != null) { @@ -204,9 +207,12 @@ namespace Barotrauma //Start drawing to the normal render target (stuff that can't be seen through the LOS effect) graphics.SetRenderTarget(renderTarget); - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, null); - spriteBatch.Draw(renderTargetBackground, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), Color.White); - spriteBatch.End(); + + graphics.BlendState = BlendState.NonPremultiplied; + graphics.SamplerStates[0] = SamplerState.LinearWrap; + Quad.UseBasicEffect(renderTargetBackground); + Quad.Render(); + //Draw the rest of the structures, characters and front structures spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, cam.Transform); Submarine.DrawBack(spriteBatch, false, e => !(e is Structure) || e.SpriteDepth < 0.9f); @@ -232,11 +238,12 @@ namespace Barotrauma //draw the rendertarget and particles that are only supposed to be drawn in water into renderTargetWater graphics.SetRenderTarget(renderTargetWater); - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Opaque); - spriteBatch.Draw(renderTarget, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), Color.White);// waterColor); - spriteBatch.End(); + graphics.BlendState = BlendState.Opaque; + graphics.SamplerStates[0] = SamplerState.LinearWrap; + Quad.UseBasicEffect(renderTarget); + Quad.Render(); - //draw alpha blended particles that are inside a sub + //draw alpha blended particles that are inside a sub spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, null, DepthStencilState.DepthRead, null, null, cam.Transform); GameMain.ParticleManager.Draw(spriteBatch, true, true, Particles.ParticleBlendState.AlphaBlend); spriteBatch.End(); @@ -257,7 +264,7 @@ namespace Barotrauma graphics.SetRenderTarget(renderTargetFinal); WaterRenderer.Instance.ResetBuffers(); - Hull.UpdateVertices(graphics, cam, WaterRenderer.Instance); + Hull.UpdateVertices(cam, WaterRenderer.Instance); WaterRenderer.Instance.RenderWater(spriteBatch, renderTargetWater, cam); WaterRenderer.Instance.RenderAir(graphics, cam, renderTarget, Cam.ShaderTransform); graphics.DepthStencilState = DepthStencilState.None; @@ -284,10 +291,12 @@ namespace Barotrauma spriteBatch.End(); if (GameMain.LightManager.LightingEnabled) { - spriteBatch.Begin(SpriteSortMode.Deferred, Lights.CustomBlendStates.Multiplicative, null, DepthStencilState.None, null, null, null); - spriteBatch.Draw(GameMain.LightManager.LightMap, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), Color.White); - spriteBatch.End(); - } + graphics.DepthStencilState = DepthStencilState.None; + graphics.SamplerStates[0] = SamplerState.LinearWrap; + graphics.BlendState = Lights.CustomBlendStates.Multiplicative; + Quad.UseBasicEffect(GameMain.LightManager.LightMap); + Quad.Render(); + } spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.LinearWrap, DepthStencilState.None, null, null, cam.Transform); foreach (Character c in Character.CharacterList) @@ -333,9 +342,12 @@ namespace Barotrauma losColor = Color.Black; } - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.PointClamp, null, null, GameMain.LightManager.LosEffect, null); - spriteBatch.Draw(renderTargetBackground, new Rectangle(0, 0, spriteBatch.GraphicsDevice.Viewport.Width, spriteBatch.GraphicsDevice.Viewport.Height), losColor); - spriteBatch.End(); + GameMain.LightManager.LosEffect.Parameters["xColor"].SetValue(losColor.ToVector4()); + + graphics.BlendState = BlendState.NonPremultiplied; + graphics.SamplerStates[0] = SamplerState.PointClamp; + GameMain.LightManager.LosEffect.CurrentTechnique.Passes[0].Apply(); + Quad.Render(); } graphics.SetRenderTarget(null); @@ -373,29 +385,23 @@ namespace Barotrauma postProcessTechnique += "Distort"; PostProcessEffect.Parameters["distortScale"].SetValue(Vector2.One * DistortStrength); PostProcessEffect.Parameters["distortUvOffset"].SetValue(WaterRenderer.Instance.WavePos * 0.001f); -#if LINUX || OSX - PostProcessEffect.Parameters["xTexture"].SetValue(distortTexture); -#else - PostProcessEffect.Parameters["xTexture"].SetValue(renderTargetFinal); -#endif } + graphics.BlendState = BlendState.Opaque; + graphics.SamplerStates[0] = SamplerState.LinearClamp; + graphics.DepthStencilState = DepthStencilState.None; if (string.IsNullOrEmpty(postProcessTechnique)) { - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Opaque, SamplerState.PointClamp, DepthStencilState.None); + Quad.UseBasicEffect(renderTargetFinal); } else { + PostProcessEffect.Parameters["MatrixTransform"].SetValue(Matrix.Identity); + PostProcessEffect.Parameters["xTexture"].SetValue(renderTargetFinal); PostProcessEffect.CurrentTechnique = PostProcessEffect.Techniques[postProcessTechnique]; PostProcessEffect.CurrentTechnique.Passes[0].Apply(); - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Opaque, SamplerState.PointClamp, DepthStencilState.None, effect: PostProcessEffect); } -#if LINUX || OSX - spriteBatch.Draw(renderTargetFinal, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), Color.White); -#else - spriteBatch.Draw(DistortStrength > 0.0f ? distortTexture : renderTargetFinal, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), Color.White); -#endif - spriteBatch.End(); + Quad.Render(); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index 842c28657..8dcd69bc0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -3,10 +3,14 @@ using Barotrauma.RuinGeneration; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; -using System.IO; using System.Linq; -using System.Xml; using System.Xml.Linq; +#if DEBUG +using System.IO; +using System.Xml; +#else +using Barotrauma.IO; +#endif namespace Barotrauma { @@ -170,14 +174,6 @@ namespace Barotrauma { base.Select(); - foreach (LevelObjectPrefab levelObjPrefab in LevelObjectPrefab.List) - { - foreach (Sprite sprite in levelObjPrefab.Sprites) - { - sprite?.EnsureLazyLoaded(); - } - } - pointerLightSource = new LightSource(Vector2.Zero, 1000.0f, Color.White, submarine: null); GameMain.LightManager.AddLight(pointerLightSource); topPanel.ClearChildren(); @@ -253,9 +249,10 @@ namespace Barotrauma }; Sprite sprite = levelObjPrefab.Sprites.FirstOrDefault() ?? levelObjPrefab.DeformableSprite?.Sprite; - GUIImage img = new GUIImage(new RectTransform(new Point(paddedFrame.Rect.Height, paddedFrame.Rect.Height - textBlock.Rect.Height), + new GUIImage(new RectTransform(new Point(paddedFrame.Rect.Height, paddedFrame.Rect.Height - textBlock.Rect.Height), paddedFrame.RectTransform, Anchor.TopCenter), sprite, scaleToFit: true) { + LoadAsynchronously = true, CanBeFocused = false }; } @@ -466,7 +463,7 @@ namespace Barotrauma Submarine.Draw(spriteBatch, false); Submarine.DrawFront(spriteBatch); Submarine.DrawDamageable(spriteBatch, null); - GUI.DrawRectangle(spriteBatch, new Rectangle(new Point(0, -Level.Loaded.Size.Y), Level.Loaded.Size), Color.White, thickness: (int)(1.0f / cam.Zoom)); + GUI.DrawRectangle(spriteBatch, new Rectangle(new Point(0, -Level.Loaded.Size.Y), Level.Loaded.Size), Color.Gray, thickness: (int)(1.0f / cam.Zoom)); spriteBatch.End(); if (lightingEnabled.Selected) @@ -504,7 +501,7 @@ namespace Barotrauma private void SerializeAll() { - XmlWriterSettings settings = new XmlWriterSettings + System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { Indent = true, NewLineOnAttributes = true @@ -585,7 +582,7 @@ namespace Barotrauma if (elementFound) { - XmlWriterSettings settings = new XmlWriterSettings + System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { Indent = true, NewLineOnAttributes = true @@ -602,7 +599,7 @@ namespace Barotrauma } - #region LevelObject Wizard +#region LevelObject Wizard private class Wizard { private LevelObjectPrefab newPrefab; @@ -683,8 +680,8 @@ namespace Barotrauma } newPrefab.Name = nameBox.Text; - - XmlWriterSettings settings = new XmlWriterSettings { Indent = true }; + + System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { Indent = true }; foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.LevelObjectPrefabs)) { XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); @@ -717,6 +714,6 @@ namespace Barotrauma } } - #endregion +#endregion } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index fdea888d7..bccff1f52 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -10,7 +10,7 @@ using RestSharp; using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Net; using System.Threading; @@ -409,10 +409,9 @@ namespace Barotrauma this.game = game; - menuTabs[(int)Tab.Credits] = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), style: null, color: Color.Black * 0.5f) - { - CanBeFocused = false - }; + menuTabs[(int)Tab.Credits] = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null); + new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, menuTabs[(int)Tab.Credits].RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); + var creditsContainer = new GUIFrame(new RectTransform(new Vector2(0.75f, 1.5f), menuTabs[(int)Tab.Credits].RectTransform, Anchor.CenterRight), style: "OuterGlow", color: Color.Black * 0.8f); creditsPlayer = new CreditsPlayer(new RectTransform(Vector2.One, creditsContainer.RectTransform), "Content/Texts/Credits.xml"); @@ -1010,11 +1009,12 @@ namespace Barotrauma GUI.Draw(Cam, spriteBatch); #if !UNSTABLE - GUI.Font.DrawString(spriteBatch, "Barotrauma v" + GameMain.Version + " (" + AssemblyInfo.GetBuildString() + ", branch " + AssemblyInfo.GetGitBranch() + ", revision " + AssemblyInfo.GetGitRevision() + ")", new Vector2(10, GameMain.GraphicsHeight - 20), Color.White * 0.7f); + string versionString = "Barotrauma v" + GameMain.Version + " (" + AssemblyInfo.GetBuildString() + ", branch " + AssemblyInfo.GetGitBranch() + ", revision " + AssemblyInfo.GetGitRevision() + ")"; + GUI.SmallFont.DrawString(spriteBatch, versionString, new Vector2(HUDLayoutSettings.Padding, GameMain.GraphicsHeight - GUI.SmallFont.MeasureString(versionString).Y - HUDLayoutSettings.Padding * 0.75f), Color.White * 0.7f); #endif if (selectedTab != Tab.Credits) { - Vector2 textPos = new Vector2(GameMain.GraphicsWidth - 10, GameMain.GraphicsHeight - 10); + Vector2 textPos = new Vector2(GameMain.GraphicsWidth - HUDLayoutSettings.Padding, GameMain.GraphicsHeight - HUDLayoutSettings.Padding * 0.75f); for (int i = legalCrap.Length - 1; i >= 0; i--) { Vector2 textSize = GUI.SmallFont.MeasureString(legalCrap[i]); @@ -1069,7 +1069,7 @@ namespace Barotrauma { File.Copy(selectedSub.FilePath, Path.Combine(SaveUtil.TempPath, selectedSub.Name + ".sub"), true); } - catch (IOException e) + catch (System.IO.IOException e) { DebugConsole.ThrowError("Copying the file \"" + selectedSub.FilePath + "\" failed. The file may have been deleted or in use by another process. Try again or select another submarine.", e); GameAnalyticsManager.AddErrorEventOnce( diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 3c29fae89..ce44807a4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -4,7 +4,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; @@ -22,6 +22,7 @@ namespace Barotrauma private GUIListBox subList, modeList; private GUIListBox chatBox, playerList; + private GUIButton serverLogReverseButton; private GUIListBox serverLogBox, serverLogFilterTicks; private GUIComponent jobVariantTooltip; @@ -64,12 +65,15 @@ namespace Barotrauma private readonly GUIButton gameModeViewButton, campaignViewButton, spectateButton; private readonly GUILayoutGroup roundControlsHolder; public GUIButton SettingsButton { get; private set; } + public static GUIButton JobInfoFrame; private readonly GUITickBox spectateBox; private readonly GUIFrame playerInfoContainer; - private GUIButton jobInfoFrame; - private GUIButton playerFrame; + + private GUILayoutGroup infoContainer; + private GUITextBlock changesPendingText; + public GUIButton PlayerFrame; private readonly GUIComponent subPreviewContainer; @@ -525,7 +529,7 @@ namespace Barotrauma if (!(serverLogHolder?.Visible ?? true)) { serverLogHolder.Visible = true; - GameMain.Client.ServerSettings.ServerLog.AssignLogFrame(serverLogBox, serverLogFilterTicks.Content, serverLogFilter); + GameMain.Client.ServerSettings.ServerLog.AssignLogFrame(serverLogReverseButton, serverLogBox, serverLogFilterTicks.Content, serverLogFilter); } showChatButton.Selected = false; showLogButton.Selected = true; @@ -609,7 +613,13 @@ namespace Barotrauma //server log ---------------------------------------------------------------------- - serverLogBox = new GUIListBox(new RectTransform(new Vector2(0.5f, 1.0f), serverLogHolderHorizontal.RectTransform)); + GUILayoutGroup serverLogListboxLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), serverLogHolderHorizontal.RectTransform)) + { + Stretch = true + }; + + serverLogReverseButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), serverLogListboxLayout.RectTransform), style: "UIToggleButtonVertical"); + serverLogBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.95f), serverLogListboxLayout.RectTransform)); //filter tickbox list ------------------------------------------------------------------ @@ -658,7 +668,7 @@ namespace Barotrauma OnClicked = (btn, obj) => { GameMain.Client.RequestStartRound(); - CoroutineManager.StartCoroutine(WaitForStartRound(StartButton, allowCancel: false), "WaitForStartRound"); + CoroutineManager.StartCoroutine(WaitForStartRound(StartButton), "WaitForStartRound"); return true; } }; @@ -776,6 +786,26 @@ namespace Barotrauma }; var subLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), subHolder.RectTransform) { MinSize = new Point(0, 25) }, TextManager.Get("Submarine"), font: GUI.SubHeadingFont); + + var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), subHolder.RectTransform), isHorizontal: true) + { + Stretch = true + }; + var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUI.Font); + var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform, Anchor.CenterRight), font: GUI.Font, createClearButton: true); + filterContainer.RectTransform.MinSize = searchBox.RectTransform.MinSize; + searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; + searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; + searchBox.OnTextChanged += (textBox, text) => + { + foreach (GUIComponent child in subList.Content.Children) + { + if (!(child.UserData is SubmarineInfo sub)) { continue; } + child.Visible = string.IsNullOrEmpty(text) ? true : sub.DisplayName.ToLower().Contains(text.ToLower()); + } + return true; + }; + subList = new GUIListBox(new RectTransform(Vector2.One, subHolder.RectTransform)) { OnSelected = VotableClicked @@ -1163,25 +1193,11 @@ namespace Barotrauma GUI.ClearCursorWait(); } - public IEnumerable WaitForStartRound(GUIButton startButton, bool allowCancel) + public IEnumerable WaitForStartRound(GUIButton startButton) { GUI.SetCursorWaiting(); string headerText = TextManager.Get("RoundStartingPleaseWait"); - var msgBox = new GUIMessageBox(headerText, TextManager.Get("RoundStarting"), - allowCancel ? new string[] { TextManager.Get("Cancel") } : new string[0]); - - if (allowCancel) - { - msgBox.Buttons[0].OnClicked = (btn, userdata) => - { - startButton.Enabled = true; - GameMain.Client.RequestRoundEnd(); - CoroutineManager.StopCoroutines("WaitForStartRound"); - GUI.ClearCursorWait(); - return true; - }; - msgBox.Buttons[0].OnClicked += msgBox.Close; - } + var msgBox = new GUIMessageBox(headerText, TextManager.Get("RoundStarting"), new string[0]); if (startButton != null) { @@ -1393,18 +1409,34 @@ namespace Barotrauma parent.ClearChildren(); - GUILayoutGroup infoContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), parent.RectTransform, Anchor.BottomCenter), childAnchor: Anchor.TopCenter) + bool isGameRunning = GameMain.GameSession?.GameMode?.IsRunning ?? false; + + infoContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, isGameRunning ? 0.95f : 0.9f), parent.RectTransform, Anchor.BottomCenter), childAnchor: Anchor.TopCenter) { - RelativeSpacing = 0.015f, + RelativeSpacing = 0.025f, Stretch = true, - UserData = characterInfo + UserData = characterInfo }; - CharacterNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.065f), infoContainer.RectTransform), characterInfo.Name, textAlignment: Alignment.Center) + bool nameChangePending = isGameRunning && GameMain.Client.PendingName != string.Empty && GameMain.Client?.Character?.Name != GameMain.Client.PendingName; + changesPendingText = null; + + if (isGameRunning) + { + infoContainer.RectTransform.AbsoluteOffset = new Point(0, (int)(parent.Rect.Height * 0.025f)); + } + + if (TabMenu.PendingChanges) + { + CreateChangesPendingText(); + } + + CharacterNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.065f), infoContainer.RectTransform), !nameChangePending ? characterInfo.Name : GameMain.Client.PendingName, textAlignment: Alignment.Center) { MaxTextLength = Client.MaxNameLength, OverflowClip = true }; + CharacterNameBox.OnEnterPressed += (tb, text) => { CharacterNameBox.Deselect(); return true; }; CharacterNameBox.OnDeselected += (tb, key) => { @@ -1417,7 +1449,15 @@ namespace Barotrauma } else { - ReadyToStartBox.Selected = false; + if (isGameRunning) + { + GameMain.Client.PendingName = tb.Text; + } + else + { + ReadyToStartBox.Selected = false; + } + GameMain.Client.SetName(tb.Text); }; }; @@ -1538,6 +1578,13 @@ namespace Barotrauma } } + private void CreateChangesPendingText() + { + if (changesPendingText != null || infoContainer == null) return; + changesPendingText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.065f), infoContainer.Parent.RectTransform, Anchor.BottomCenter, Pivot.TopCenter) { RelativeOffset = new Vector2(0f, -0.065f) }, + TextManager.Get("tabmenu.characterchangespending"), textColor: GUI.Style.Orange, textAlignment: Alignment.Center, style: null) { IgnoreLayoutGroups = true }; + } + private void CreateJobVariantTooltip(JobPrefab jobPrefab, int variant, GUIComponent parentSlot) { jobVariantTooltip = new GUIFrame(new RectTransform(new Point((int)(500 * GUI.Scale), (int)(200 * GUI.Scale)), GUI.Canvas, pivot: Pivot.BottomRight), @@ -1845,28 +1892,28 @@ namespace Barotrauma public void SetPlayerNameAndJobPreference(Client client) { - var playerFrame = (GUITextBlock)PlayerList.Content.FindChild(client); - if (playerFrame == null) { return; } - playerFrame.Text = client.Name; + var PlayerFrame = (GUITextBlock)PlayerList.Content.FindChild(client); + if (PlayerFrame == null) { return; } + PlayerFrame.Text = client.Name; Color color = Color.White; if (JobPrefab.Prefabs.ContainsKey(client.PreferredJob)) { color = JobPrefab.Prefabs[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; + 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(c => c.UserData is Pair pair && pair.First == "soundicon"); - var soundIconDisabled = playerFrame.FindChild("soundicondisabled"); + var PlayerFrame = PlayerList.Content.FindChild(client); + if (PlayerFrame == null) { return; } + var soundIcon = PlayerFrame.FindChild(c => c.UserData is Pair pair && pair.First == "soundicon"); + var soundIconDisabled = PlayerFrame.FindChild("soundicondisabled"); Pair userdata = soundIcon.UserData as Pair; @@ -1881,9 +1928,9 @@ namespace Barotrauma public void SetPlayerSpeaking(Client client) { - var playerFrame = PlayerList.Content.FindChild(client); - if (playerFrame == null) { return; } - var soundIcon = playerFrame.FindChild(c => c.UserData is Pair pair && pair.First == "soundicon"); + var PlayerFrame = PlayerList.Content.FindChild(client); + if (PlayerFrame == null) { return; } + var soundIcon = PlayerFrame.FindChild(c => c.UserData is Pair pair && pair.First == "soundicon"); Pair userdata = soundIcon.UserData as Pair; userdata.Second = Math.Max(userdata.Second, 0.18f); soundIcon.Visible = true; @@ -1895,51 +1942,38 @@ namespace Barotrauma if (child != null) { playerList.RemoveChild(child); } } - private bool SelectPlayer(Client selectedClient) + public bool SelectPlayer(Client selectedClient) { bool myClient = selectedClient.ID == GameMain.Client.ID; + bool hasManagePermissions = GameMain.Client.HasPermission(ClientPermissions.ManagePermissions); - playerFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas), style: "GUIBackgroundBlocker") + PlayerFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null) { OnClicked = (btn, userdata) => { if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) ClosePlayerFrame(btn, userdata); return true; } }; - Vector2 frameSize = GameMain.Client.HasPermission(ClientPermissions.ManagePermissions) ? new Vector2(.28f, .5f) : new Vector2(.28f, .24f); + new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, PlayerFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); + Vector2 frameSize = hasManagePermissions ? new Vector2(.28f, .5f) : new Vector2(.28f, .15f); - var playerFrameInner = new GUIFrame(new RectTransform(frameSize, playerFrame.RectTransform, Anchor.Center) { MinSize = new Point(550, 0) }); + var playerFrameInner = new GUIFrame(new RectTransform(frameSize, PlayerFrame.RectTransform, Anchor.Center) { MinSize = new Point(550, 0) }); var paddedPlayerFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.88f), playerFrameInner.RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.03f }; - var headerContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), paddedPlayerFrame.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) + var headerContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, hasManagePermissions ? 0.1f : 0.25f), paddedPlayerFrame.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; - var nameText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), headerContainer.RectTransform), + var nameText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), headerContainer.RectTransform), text: selectedClient.Name, font: GUI.LargeFont); - nameText.Text = ToolBox.LimitString(nameText.Text, nameText.Font, nameText.Rect.Width); + nameText.Text = ToolBox.LimitString(nameText.Text, nameText.Font, (int)(nameText.Rect.Width * 0.95f)); - if (selectedClient.SteamID != 0 && Steam.SteamManager.IsInitialized) + if (hasManagePermissions) { - var viewSteamProfileButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), headerContainer.RectTransform, Anchor.TopCenter) { MaxSize = new Point(int.MaxValue, (int)(40 * GUI.Scale)) }, - TextManager.Get("ViewSteamProfile")) - { - UserData = selectedClient - }; - viewSteamProfileButton.TextBlock.AutoScaleHorizontal = true; - viewSteamProfileButton.OnClicked = (bt, userdata) => - { - Steamworks.SteamFriends.OpenWebOverlay("https://steamcommunity.com/profiles/" + selectedClient.SteamID.ToString()); - return true; - }; - } - - if (GameMain.Client.HasPermission(ClientPermissions.ManagePermissions)) - { - playerFrame.UserData = selectedClient; + PlayerFrame.UserData = selectedClient; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), paddedPlayerFrame.RectTransform), TextManager.Get("Rank"), font: GUI.SubHeadingFont); @@ -1965,11 +1999,11 @@ namespace Barotrauma PermissionPreset selectedPreset = (PermissionPreset)userdata; if (selectedPreset != null) { - var client = playerFrame.UserData as Client; + var client = PlayerFrame.UserData as Client; client.SetPermissions(selectedPreset.Permissions, selectedPreset.PermittedCommands); GameMain.Client.UpdateClientPermissions(client); - playerFrame = null; + PlayerFrame = null; SelectPlayer(client); } return true; @@ -2005,7 +2039,7 @@ namespace Barotrauma //reset rank to custom rankDropDown.SelectItem(null); - if (!(playerFrame.UserData is Client client)) { return false; } + if (!(PlayerFrame.UserData is Client client)) { return false; } foreach (GUIComponent child in tickbox.Parent.GetChild().Content.Children) { @@ -2038,7 +2072,7 @@ namespace Barotrauma //reset rank to custom rankDropDown.SelectItem(null); - if (!(playerFrame.UserData is Client client)) { return false; } + if (!(PlayerFrame.UserData is Client client)) { return false; } var thisPermission = (ClientPermissions)tickBox.UserData; if (tickBox.Selected) @@ -2072,7 +2106,7 @@ namespace Barotrauma //reset rank to custom rankDropDown.SelectItem(null); - if (!(playerFrame.UserData is Client client)) { return false; } + if (!(PlayerFrame.UserData is Client client)) { return false; } foreach (GUIComponent child in tickbox.Parent.GetChild().Content.Children) { @@ -2105,7 +2139,7 @@ namespace Barotrauma rankDropDown.SelectItem(null); DebugConsole.Command selectedCommand = tickBox.UserData as DebugConsole.Command; - if (!(playerFrame.UserData is Client client)) { return false; } + if (!(PlayerFrame.UserData is Client client)) { return false; } if (!tickBox.Selected) { @@ -2125,7 +2159,7 @@ namespace Barotrauma } var buttonAreaTop = myClient ? null : new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.08f), paddedPlayerFrame.RectTransform), isHorizontal: true); - var buttonAreaLower = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.08f), paddedPlayerFrame.RectTransform), isHorizontal: true); + var buttonAreaLower = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.08f), paddedPlayerFrame.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); if (!myClient) { @@ -2173,32 +2207,66 @@ namespace Barotrauma kickButton.OnClicked += ClosePlayerFrame; } - GUITextBlock.AutoScaleAndNormalize( - buttonAreaTop.Children.Select(c => ((GUIButton)c).TextBlock).Concat(buttonAreaLower.Children.Select(c => ((GUIButton)c).TextBlock))); + if (buttonAreaTop.CountChildren > 0) + { + GUITextBlock.AutoScaleAndNormalize(buttonAreaTop.Children.Select(c => ((GUIButton)c).TextBlock).Concat(buttonAreaLower.Children.Select(c => ((GUIButton)c).TextBlock))); + } - new GUITickBox(new RectTransform(new Vector2(0.25f, 1.0f), buttonAreaTop.RectTransform, Anchor.TopRight), + new GUITickBox(new RectTransform(new Vector2(0.175f, 1.0f), headerContainer.RectTransform, Anchor.TopRight), TextManager.Get("Mute")) { - IgnoreLayoutGroups = true, Selected = selectedClient.MutedLocally, OnSelected = (tickBox) => { selectedClient.MutedLocally = tickBox.Selected; return true; } }; } - var closeButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonAreaLower.RectTransform, Anchor.TopRight), + if (selectedClient.SteamID != 0 && Steam.SteamManager.IsInitialized) + { + var viewSteamProfileButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), headerContainer.RectTransform, Anchor.TopCenter) { MaxSize = new Point(int.MaxValue, (int)(40 * GUI.Scale)) }, + TextManager.Get("ViewSteamProfile")) + { + UserData = selectedClient + }; + viewSteamProfileButton.TextBlock.AutoScaleHorizontal = true; + viewSteamProfileButton.OnClicked = (bt, userdata) => + { + Steamworks.SteamFriends.OpenWebOverlay("https://steamcommunity.com/profiles/" + selectedClient.SteamID.ToString()); + return true; + }; + } + + var closeButton = new GUIButton(new RectTransform(new Vector2(0f, 1.0f), buttonAreaLower.RectTransform, Anchor.CenterRight), TextManager.Get("Close")) { IgnoreLayoutGroups = true, OnClicked = ClosePlayerFrame }; + float xSize = 1f / buttonAreaLower.CountChildren; + for (int i = 0; i < buttonAreaLower.CountChildren; i++) + { + buttonAreaLower.GetChild(i).RectTransform.RelativeSize = new Vector2(xSize, 1f); + } + buttonAreaLower.RectTransform.NonScaledSize = new Point(buttonAreaLower.Rect.Width, buttonAreaLower.RectTransform.Children.Max(c => c.NonScaledSize.Y)); if (buttonAreaTop != null) { - buttonAreaTop.RectTransform.NonScaledSize = - buttonAreaLower.RectTransform.NonScaledSize = - new Point(buttonAreaLower.Rect.Width, Math.Max(buttonAreaLower.RectTransform.NonScaledSize.Y, buttonAreaTop.RectTransform.Children.Max(c => c.NonScaledSize.Y))); + if (buttonAreaTop.CountChildren == 0) + { + paddedPlayerFrame.RemoveChild(buttonAreaTop); + } + else + { + for (int i = 0; i < buttonAreaTop.CountChildren; i++) + { + buttonAreaTop.GetChild(i).RectTransform.RelativeSize = new Vector2(1f / 3f, 1f); + } + + buttonAreaTop.RectTransform.NonScaledSize = + buttonAreaLower.RectTransform.NonScaledSize = + new Point(buttonAreaLower.Rect.Width, Math.Max(buttonAreaLower.RectTransform.NonScaledSize.Y, buttonAreaTop.RectTransform.Children.Max(c => c.NonScaledSize.Y))); + } } return false; @@ -2206,7 +2274,7 @@ namespace Barotrauma private bool ClosePlayerFrame(GUIButton button, object userData) { - playerFrame = null; + PlayerFrame = null; playerList.Deselect(); return true; } @@ -2233,9 +2301,8 @@ namespace Barotrauma { base.AddToGUIUpdateList(); - playerFrame?.AddToGUIUpdateList(); //CampaignSetupUI?.AddToGUIUpdateList(); - jobInfoFrame?.AddToGUIUpdateList(); + JobInfoFrame?.AddToGUIUpdateList(); HeadSelectionList?.AddToGUIUpdateList(); JobSelectionFrame?.AddToGUIUpdateList(); @@ -2532,6 +2599,7 @@ namespace Barotrauma StepValue = 1, BarScrollValue = info.HairIndex, OnMoved = SwitchHair, + OnReleased = SaveHead, BarSize = 1.0f / (float)(hairCount + 1) }; } @@ -2546,6 +2614,7 @@ namespace Barotrauma StepValue = 1, BarScrollValue = info.BeardIndex, OnMoved = SwitchBeard, + OnReleased = SaveHead, BarSize = 1.0f / (float)(beardCount + 1) }; } @@ -2560,6 +2629,7 @@ namespace Barotrauma StepValue = 1, BarScrollValue = info.MoustacheIndex, OnMoved = SwitchMoustache, + OnReleased = SaveHead, BarSize = 1.0f / (float)(moustacheCount + 1) }; } @@ -2574,6 +2644,7 @@ namespace Barotrauma StepValue = 1, BarScrollValue = info.FaceAttachmentIndex, OnMoved = SwitchFaceAttachment, + OnReleased = SaveHead, BarSize = 1.0f / (float)(faceAttachmentCount + 1) }; } @@ -2881,7 +2952,7 @@ namespace Barotrauma var textBlock = new GUITextBlock( innerFrame.CountChildren == 0 ? new RectTransform(Vector2.One, parent.RectTransform, Anchor.Center) : - new RectTransform(new Vector2(selectedByPlayer ? 0.65f : 0.95f, 0.3f), parent.RectTransform, Anchor.BottomCenter), + new RectTransform(new Vector2(selectedByPlayer ? 0.55f : 0.95f, 0.3f), parent.RectTransform, Anchor.BottomCenter), jobPrefab.Name, wrap: true, textAlignment: Alignment.BottomCenter) { Padding = Vector4.Zero, @@ -2910,7 +2981,7 @@ namespace Barotrauma info.Head = new CharacterInfo.HeadInfo(id, gender, race); info.ReloadHeadAttachments(); } - StoreHead(); + StoreHead(true); UpdateJobPreferences(JobList); @@ -2918,7 +2989,8 @@ namespace Barotrauma return true; } - + + private bool SaveHead(GUIScrollBar scrollBar, float barScroll) => StoreHead(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); @@ -2946,14 +3018,15 @@ namespace Barotrauma return false; } info.ReloadHeadAttachments(); - StoreHead(); + StoreHead(false); return true; } - private void StoreHead() + private bool StoreHead(bool save) { var info = GameMain.Client.CharacterInfo; var config = GameMain.Config; + config.CharacterRace = info.Race; config.CharacterGender = info.Gender; config.CharacterHeadIndex = info.HeadSpriteId; @@ -2961,8 +3034,22 @@ namespace Barotrauma config.CharacterBeardIndex = info.BeardIndex; config.CharacterMoustacheIndex = info.MoustacheIndex; config.CharacterFaceAttachmentIndex = info.FaceAttachmentIndex; + + if (save) + { + if (GameMain.GameSession?.GameMode?.IsRunning ?? false) + { + TabMenu.PendingChanges = true; + CreateChangesPendingText(); + } + + GameMain.Config.SaveNewPlayerConfig(); + } + + return true; } + public void SelectMode(int modeIndex) { if (modeIndex < 0 || modeIndex >= modeList.Content.CountChildren) { return; } @@ -3026,7 +3113,7 @@ namespace Barotrauma StartRound = () => { GameMain.Client.RequestStartRound(); - CoroutineManager.StartCoroutine(WaitForStartRound(campaignUI.StartButton, allowCancel: true), "WaitForStartRound"); + CoroutineManager.StartCoroutine(WaitForStartRound(campaignUI.StartButton), "WaitForStartRound"); } }; @@ -3085,20 +3172,20 @@ namespace Barotrauma { if (!(button.UserData is Pair jobPrefab)) { return false; } - jobInfoFrame = jobPrefab.First.CreateInfoFrame(jobPrefab.Second); - GUIButton closeButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.05f), jobInfoFrame.GetChild(2).GetChild(0).RectTransform, Anchor.BottomRight), + JobInfoFrame = jobPrefab.First.CreateInfoFrame(jobPrefab.Second); + GUIButton closeButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.05f), JobInfoFrame.GetChild(2).GetChild(0).RectTransform, Anchor.BottomRight), TextManager.Get("Close")) { OnClicked = CloseJobInfo }; - jobInfoFrame.OnClicked = (btn, userdata) => { if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) CloseJobInfo(btn, userdata); return true; }; + JobInfoFrame.OnClicked = (btn, userdata) => { if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) CloseJobInfo(btn, userdata); return true; }; return true; } private bool CloseJobInfo(GUIButton button, object obj) { - jobInfoFrame = null; + JobInfoFrame = null; return true; } @@ -3192,8 +3279,14 @@ namespace Barotrauma } GameMain.Client.ForceNameAndJobUpdate(); - if (!GameMain.Config.JobPreferences.SequenceEqual(jobNamePreferences)) + if (!GameMain.Config.AreJobPreferencesEqual(jobNamePreferences)) { + if (GameMain.GameSession?.GameMode?.IsRunning ?? false) + { + TabMenu.PendingChanges = true; + CreateChangesPendingText(); + } + GameMain.Config.JobPreferences = jobNamePreferences; GameMain.Config.SaveNewPlayerConfig(); } @@ -3238,7 +3331,8 @@ namespace Barotrauma { CreateSubPreview(sub); } - if (subList.SelectedData is SubmarineInfo selectedSub && selectedSub.MD5Hash?.Hash == md5Hash && System.IO.File.Exists(sub.FilePath)) + + if (subList.SelectedData is SubmarineInfo selectedSub && selectedSub.MD5Hash?.Hash == md5Hash && Barotrauma.IO.File.Exists(sub.FilePath)) { return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs index d37df90f9..5d7eba886 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs @@ -4,9 +4,14 @@ using Barotrauma.Particles; using System; using System.Collections.Generic; using System.Xml.Linq; -using System.Xml; using System.Text; using Barotrauma.Extensions; +#if DEBUG +using System.IO; +using System.Xml; +#else +using Barotrauma.IO; +#endif namespace Barotrauma { @@ -242,7 +247,7 @@ namespace Barotrauma } } - XmlWriterSettings settings = new XmlWriterSettings + System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { Indent = true, OmitXmlDeclaration = true, @@ -260,27 +265,49 @@ namespace Barotrauma private void SerializeToClipboard(ParticlePrefab prefab) { #if WINDOWS - if (prefab == null) return; + if (prefab == null) { return; } - XmlWriterSettings settings = new XmlWriterSettings + System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { Indent = true, OmitXmlDeclaration = true, NewLineOnAttributes = true }; - XElement element = new XElement(prefab.Name); - SerializableProperty.SerializeProperties(prefab, element, true); + XElement originalElement = null; + foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.Particles)) + { + XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); + if (doc == null) { continue; } + + var prefabList = GameMain.ParticleManager.GetPrefabList(); + foreach (ParticlePrefab otherPrefab in prefabList) + { + foreach (XElement subElement in doc.Root.Elements()) + { + if (!subElement.Name.ToString().Equals(prefab.Name, StringComparison.OrdinalIgnoreCase)) { continue; } + SerializableProperty.SerializeProperties(prefab, subElement, true); + originalElement = subElement; + break; + } + } + } + + if (originalElement == null) + { + originalElement = new XElement(prefab.Name); + SerializableProperty.SerializeProperties(prefab, originalElement, true); + } StringBuilder sb = new StringBuilder(); - using (var writer = XmlWriter.Create(sb, settings)) + using (var writer = System.Xml.XmlWriter.Create(sb, settings)) { - element.WriteTo(writer); + originalElement.WriteTo(writer); writer.Flush(); } Clipboard.SetText(sb.ToString()); -#endif +#endif } public override void Update(double deltaTime) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs index 4b0fcf608..08266e537 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/Screen.cs @@ -14,10 +14,11 @@ namespace Barotrauma { if (frame == null) { - frame = new GUIFrame(new RectTransform(Vector2.One, GUICanvas.Instance), style: null) + frame = new GUIFrame(new RectTransform(GUICanvas.Instance.RelativeSize, GUICanvas.Instance), style: null) { CanBeFocused = false }; + } return frame; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs index d3df520ff..4f0ab96eb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs @@ -6,7 +6,7 @@ using Microsoft.Xna.Framework.Graphics; using RestSharp; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Net; using System.Net.NetworkInformation; @@ -422,7 +422,7 @@ namespace Barotrauma } // Game mode Selection - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("gamemode")) { CanBeFocused = false }; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("gamemode"), font: GUI.SubHeadingFont) { CanBeFocused = false }; gameModeTickBoxes = new List(); foreach (GameModePreset mode in GameModePreset.List) @@ -474,7 +474,7 @@ namespace Barotrauma }; btn.Color *= 0.5f; labelTexts.Add(btn.TextBlock); - + new GUIImage(new RectTransform(new Vector2(0.5f, 0.3f), btn.RectTransform, Anchor.BottomCenter, scaleBasis: ScaleBasis.BothHeight), style: "GUIButtonVerticalArrow", scaleToFit: true) { CanBeFocused = false, @@ -567,7 +567,18 @@ namespace Barotrauma var directJoinButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform), TextManager.Get("serverlistdirectjoin")) { - OnClicked = (btn, userdata) => { ShowDirectJoinPrompt(); return true; } + OnClicked = (btn, userdata) => + { + if (string.IsNullOrWhiteSpace(ClientNameBox.Text)) + { + ClientNameBox.Flash(); + ClientNameBox.Select(); + GUI.PlayUISound(GUISoundType.PickItemFail); + return false; + } + ShowDirectJoinPrompt(); + return true; + } }; joinButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform), @@ -672,7 +683,21 @@ namespace Barotrauma if (!File.Exists(file)) { return; } XDocument doc = XMLExtensions.TryLoadXml(file); - if (doc == null) { return; } + if (doc == null) + { + DebugConsole.NewMessage("Failed to load file \"" + file + "\". Attempting to recreate the file..."); + try + { + doc = new XDocument(new XElement("servers")); + doc.Save(file); + DebugConsole.NewMessage("Recreated \"" + file + "\"."); + } + catch (Exception e) + { + DebugConsole.ThrowError("Failed to recreate the file \"" + file + "\".", e); + } + return; + } foreach (XElement element in doc.Root.Elements()) { @@ -694,7 +719,7 @@ namespace Barotrauma rootElement.Add(info.ToXElement()); } - doc.Save(file); + doc.SaveSafe(file); } public ServerInfo UpdateServerInfoWithServerSettings(object endpoint, ServerSettings serverSettings) @@ -909,6 +934,12 @@ namespace Barotrauma Steamworks.SteamMatchmaking.ResetActions(); + if (GameMain.Client != null) + { + GameMain.Client.Disconnect(); + GameMain.Client = null; + } + RefreshServers(); } @@ -996,7 +1027,7 @@ namespace Barotrauma foreach (GUITickBox tickBox in gameModeTickBoxes) { var gameMode = (string)tickBox.UserData; - if (!tickBox.Selected && serverInfo.GameMode.Equals(gameMode, StringComparison.OrdinalIgnoreCase)) + if (!tickBox.Selected && serverInfo.GameMode != null && serverInfo.GameMode.Equals(gameMode, StringComparison.OrdinalIgnoreCase)) { child.Visible = false; break; @@ -1653,7 +1684,7 @@ namespace Barotrauma { CanBeFocused = false, Selected = - serverInfo.GameVersion == GameMain.Version.ToString() && + (NetworkMember.IsCompatible(GameMain.Version.ToString(), serverInfo.GameVersion) ?? true) && serverInfo.ContentPackagesMatch(GameMain.SelectedPackages), UserData = "compatible" }; @@ -1679,6 +1710,14 @@ namespace Barotrauma serverName.Text = ToolBox.LimitString(serverName.Text, serverName.Font, serverName.Rect.Width); }; + if (serverInfo.ContentPackageNames.Any()) + { + if (serverInfo.ContentPackageNames.Any(cp => !cp.Equals(GameMain.VanillaContent.Name, StringComparison.OrdinalIgnoreCase))) + { + serverName.TextColor = new Color(219, 125, 217); + } + } + new GUITickBox(new RectTransform(new Vector2(columnRelativeWidth[3], 0.9f), serverContent.RectTransform, Anchor.Center), label: "") { ToolTip = TextManager.Get((serverInfo.GameStarted) ? "ServerListRoundStarted" : "ServerListRoundNotStarted"), @@ -1897,6 +1936,8 @@ namespace Barotrauma if (string.IsNullOrWhiteSpace(ClientNameBox.Text)) { ClientNameBox.Flash(); + ClientNameBox.Select(); + GUI.PlayUISound(GUISoundType.PickItemFail); return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs index 3fc91dcf5..932d9a409 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs @@ -3,10 +3,14 @@ using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Xml.Linq; using Barotrauma.Extensions; +#if DEBUG +using System.IO; +#else +using Barotrauma.IO; +#endif namespace Barotrauma { @@ -196,8 +200,9 @@ namespace Barotrauma Stretch = true, UserData = "filterarea" }; - filterTexturesLabel = new GUITextBlock(new RectTransform(Vector2.One, filterArea.RectTransform), TextManager.Get("serverlog.filter"), font: GUI.Font) { IgnoreLayoutGroups = true }; ; + filterTexturesLabel = new GUITextBlock(new RectTransform(Vector2.One, filterArea.RectTransform), TextManager.Get("serverlog.filter"), font: GUI.Font, textAlignment: Alignment.CenterLeft) { IgnoreLayoutGroups = true }; ; filterTexturesBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1.0f), filterArea.RectTransform), font: GUI.Font, createClearButton: true); + filterArea.RectTransform.MinSize = filterTexturesBox.RectTransform.MinSize; filterTexturesBox.OnTextChanged += (textBox, text) => { FilterTextures(text); return true; }; textureList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), paddedLeftPanel.RectTransform)) @@ -240,8 +245,9 @@ namespace Barotrauma Stretch = true, UserData = "filterarea" }; - filterSpritesLabel = new GUITextBlock(new RectTransform(Vector2.One, filterArea.RectTransform), TextManager.Get("serverlog.filter"), font: GUI.Font) { IgnoreLayoutGroups = true }; + filterSpritesLabel = new GUITextBlock(new RectTransform(Vector2.One, filterArea.RectTransform), TextManager.Get("serverlog.filter"), font: GUI.Font, textAlignment: Alignment.CenterLeft) { IgnoreLayoutGroups = true }; filterSpritesBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1.0f), filterArea.RectTransform), font: GUI.Font, createClearButton: true); + filterArea.RectTransform.MinSize = filterSpritesBox.RectTransform.MinSize; filterSpritesBox.OnTextChanged += (textBox, text) => { FilterSprites(text); return true; }; spriteList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), paddedRightPanel.RectTransform)) @@ -413,7 +419,11 @@ namespace Barotrauma { string xmlPath = doc.ParseContentPathFromUri(); xmlPathText.Text += "\n" + xmlPath; +#if DEBUG doc.Save(xmlPath); +#else + doc.SaveSafe(xmlPath); +#endif } xmlPathText.TextColor = GUI.Style.Green; return true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs index 5d66494e0..1833713a8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs @@ -4,7 +4,7 @@ using Microsoft.Xna.Framework.Graphics; using RestSharp; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -26,7 +26,7 @@ namespace Barotrauma //listbox that shows the files included in the item being created private GUIListBox createItemFileList; - private FileSystemWatcher createItemWatcher; + private System.IO.FileSystemWatcher createItemWatcher; private readonly List tabButtons = new List(); @@ -135,6 +135,8 @@ namespace Barotrauma } }; + CreateFilterBox(modsContainer, subscribedItemList); + modsPreviewFrame = new GUIFrame(new RectTransform(new Vector2(0.6f, 1.0f), tabs[(int)Tab.Mods].RectTransform, Anchor.TopRight), style: null); //------------------------------------------------------------------------------- @@ -163,6 +165,8 @@ namespace Barotrauma } }; + CreateFilterBox(listContainer, topItemList); + new GUIButton(new RectTransform(new Vector2(1.0f, 0.02f), listContainer.RectTransform), TextManager.Get("FindModsButton"), style: "GUIButtonSmall") { OnClicked = (btn, userdata) => @@ -235,6 +239,29 @@ namespace Barotrauma subscribedCoroutine = CoroutineManager.StartCoroutine(PollSubscribedItems()); } + private void CreateFilterBox(GUIComponent parent, GUIListBox listbox) + { + var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), parent.RectTransform), isHorizontal: true) + { + Stretch = true + }; + filterContainer.RectTransform.SetAsFirstChild(); + var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUI.Font); + var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform, Anchor.CenterRight), font: GUI.Font, createClearButton: true); + filterContainer.RectTransform.MinSize = searchBox.RectTransform.MinSize; + searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; + searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; + searchBox.OnTextChanged += (textBox, text) => + { + foreach (GUIComponent child in listbox.Content.Children) + { + if (!(child.UserData is Steamworks.Ugc.Item item)) { continue; } + child.Visible = string.IsNullOrEmpty(text) ? true : (item.Title?.ToLower().Contains(text.ToLower()) ?? false); + } + return true; + }; + } + public override void Select() { base.Select(); @@ -414,6 +441,7 @@ namespace Barotrauma foreach (ContentPackage contentPackage in ContentPackage.List) { if (!string.IsNullOrEmpty(contentPackage.SteamWorkshopUrl) || contentPackage.HideInWorkshopMenu) { continue; } + if (contentPackage == GameMain.VanillaContent) { continue; } //don't list content packages that only define one sub (they're visible in the "Submarines" section) if (contentPackage.Files.Count == 1 && contentPackage.Files[0].Type == ContentType.Submarine) { continue; } CreateMyItemFrame(contentPackage, myItemList); @@ -582,14 +610,12 @@ namespace Barotrauma } else { - installed = SteamManager.EnableWorkShopItem(item, true, out string errorMsg, Screen.Selected == this); - + installed = SteamManager.EnableWorkShopItem(item, out string errorMsg, Selected == this); if (!installed) { - DebugConsole.ThrowError(errorMsg); - new GUIMessageBox( - TextManager.Get("Error"), - TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { TextManager.EnsureUTF8(item?.Title), errorMsg })); + DebugConsole.NewMessage(errorMsg, Color.Red); + titleText.TextColor = Color.Red; + titleText.ToolTip = itemFrame.ToolTip = TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { TextManager.EnsureUTF8(item?.Title), errorMsg }); } } } @@ -602,10 +628,9 @@ namespace Barotrauma { if (!SteamManager.UpdateWorkshopItem(item, out string errorMsg)) { - DebugConsole.ThrowError(errorMsg); - new GUIMessageBox( - TextManager.Get("Error"), - TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { TextManager.EnsureUTF8(item?.Title), errorMsg })); + DebugConsole.NewMessage(errorMsg, Color.Red); + titleText.TextColor = Color.Red; + titleText.ToolTip = itemFrame.ToolTip = TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { TextManager.EnsureUTF8(item?.Title), errorMsg }); } } } @@ -645,7 +670,7 @@ namespace Barotrauma { bool reselect = GameMain.Config.SelectedContentPackages.Any(cp => !string.IsNullOrWhiteSpace(cp.SteamWorkshopUrl) && cp.SteamWorkshopUrl == item?.Url); if (!SteamManager.DisableWorkShopItem(item, false, out string errorMsg) || - !SteamManager.EnableWorkShopItem(item, true, out errorMsg, reselect, true)) + !SteamManager.EnableWorkShopItem(item, out errorMsg, reselect, true)) { DebugConsole.ThrowError($"Failed to reinstall \"{item?.Title}\": {errorMsg}", null, true); elem.Flash(GUI.Style.Red); @@ -839,7 +864,7 @@ namespace Barotrauma item?.Download(onInstalled: () => { - if (SteamManager.EnableWorkShopItem(item, false, out _)) + if (SteamManager.EnableWorkShopItem(item, out _)) { textBlock.Text = TextManager.Get("workshopiteminstalled"); frame.Flash(GUI.Style.Green); @@ -1012,7 +1037,7 @@ namespace Barotrauma private void CreateWorkshopItem(SubmarineInfo sub) { - string destinationFolder = Path.Combine("Mods", sub.Name); + string destinationFolder = Path.Combine("Mods", sub.Name.Trim()); itemContentPackage = ContentPackage.CreatePackage(sub.Name, Path.Combine(destinationFolder, SteamManager.MetadataFileName), corePackage: false); SteamManager.CreateWorkshopItemStaging(itemContentPackage, out itemEditor); @@ -1040,7 +1065,7 @@ namespace Barotrauma string previewImagePath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(itemContentPackage.Path), SteamManager.PreviewImageName)); try { - using (Stream s = File.Create(previewImagePath)) + using (System.IO.Stream s = File.Create(previewImagePath)) { sub.PreviewImage.Texture.SaveAsPng(s, (int)sub.PreviewImage.size.X, (int)sub.PreviewImage.size.Y); itemEditor = itemEditor?.WithPreviewFile(previewImagePath); @@ -1292,10 +1317,10 @@ namespace Barotrauma }; createItemFileList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.35f), createItemContent.RectTransform)); createItemWatcher?.Dispose(); - createItemWatcher = new FileSystemWatcher(Path.GetDirectoryName(itemContentPackage.Path)) + createItemWatcher = new System.IO.FileSystemWatcher(Path.GetDirectoryName(itemContentPackage.Path)) { Filter = "*", - NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName + NotifyFilter = System.IO.NotifyFilters.LastWrite | System.IO.NotifyFilters.FileName | System.IO.NotifyFilters.DirectoryName }; createItemWatcher.Created += OnFileSystemChanges; createItemWatcher.Deleted += OnFileSystemChanges; @@ -1553,7 +1578,7 @@ namespace Barotrauma volatile bool refreshFileList = false; - private void OnFileSystemChanges(object sender, FileSystemEventArgs e) + private void OnFileSystemChanges(object sender, System.IO.FileSystemEventArgs e) { refreshFileList = true; } @@ -1566,14 +1591,26 @@ namespace Barotrauma List files = itemContentPackage.Files.ToList(); - foreach (ContentFile contentFile in files) + for (int i = files.Count - 1; i >= 0; i--) { + ContentFile contentFile = files[i]; + bool fileExists = File.Exists(contentFile.Path); - if (!fileExists) { itemContentPackage.Files.Remove(contentFile); continue; } + if (contentFile.Type == ContentType.Executable || + contentFile.Type == ContentType.ServerExecutable) + { + fileExists |= File.Exists(contentFile.Path + ".dll"); + } + + if (!fileExists) + { + itemContentPackage.Files.Remove(contentFile); + files.RemoveAt(i); + } } - List allFiles = Directory.GetFiles(Path.GetDirectoryName(itemContentPackage.Path), "*", SearchOption.AllDirectories) + List allFiles = Directory.GetFiles(Path.GetDirectoryName(itemContentPackage.Path), "*", System.IO.SearchOption.AllDirectories) .Select(f => new ContentFile(f, ContentType.None)) .Where(file => Path.GetFileName(file.Path) != SteamManager.MetadataFileName && Path.GetFileName(file.Path) != SteamManager.PreviewImageName) @@ -1581,22 +1618,31 @@ namespace Barotrauma for (int i=0;i string.Equals(Path.GetFullPath(f.Path).CleanUpPath(), - Path.GetFullPath(file.Path).CleanUpPath(), - StringComparison.InvariantCultureIgnoreCase)); + ContentFile otherFile = files.Find(f => string.Equals(Path.GetFullPath(f.Path).CleanUpPath(), + Path.GetFullPath(file.Path).CleanUpPath(), + StringComparison.InvariantCultureIgnoreCase)); if (otherFile != null) { //replace the generated ContentFile object with the one that's present in the //content package to determine which tickboxes should already be checked allFiles[i] = otherFile; + files.Remove(otherFile); } } + allFiles.AddRange(files); + foreach (ContentFile contentFile in allFiles) { bool illegalPath = !ContentPackage.IsModFilePathAllowed(contentFile); bool fileExists = File.Exists(contentFile.Path); + if (contentFile.Type == ContentType.Executable || + contentFile.Type == ContentType.ServerExecutable) + { + fileExists |= File.Exists(contentFile.Path + ".dll"); + } + var fileFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.12f), createItemFileList.Content.RectTransform) { MinSize = new Point(0, 20) }, style: "ListBoxElement") { @@ -1664,36 +1710,39 @@ namespace Barotrauma return true; }; - new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), content.RectTransform), TextManager.Get("Delete"), style: "GUIButtonSmall") + if (!files.Contains(contentFile)) //this prevents deletion of files not contained in the mod's path (i.e. vanilla content) { - OnClicked = (btn, userdata) => + new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), content.RectTransform), TextManager.Get("Delete"), style: "GUIButtonSmall") { - var msgBox = new GUIMessageBox(TextManager.Get("ConfirmFileDeletionHeader"), - TextManager.GetWithVariable("ConfirmFileDeletion", "[file]", contentFile.Path), - new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }) + OnClicked = (btn, userdata) => { - UserData = "verificationprompt" - }; - msgBox.Buttons[0].OnClicked = (applyButton, obj) => - { - try + var msgBox = new GUIMessageBox(TextManager.Get("ConfirmFileDeletionHeader"), + TextManager.GetWithVariable("ConfirmFileDeletion", "[file]", contentFile.Path), + new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }) { - File.Delete(contentFile.Path); - if (contentFile.Type == ContentType.Submarine) { SubmarineInfo.RefreshSavedSub(contentFile.Path); } - } - catch (Exception e) + UserData = "verificationprompt" + }; + msgBox.Buttons[0].OnClicked = (applyButton, obj) => { - DebugConsole.ThrowError($"Failed to delete \"${contentFile.Path}\".", e); - } - //RefreshCreateItemFileList(); - RefreshMyItemList(); + try + { + File.Delete(contentFile.Path); + if (contentFile.Type == ContentType.Submarine) { SubmarineInfo.RefreshSavedSub(contentFile.Path); } + } + catch (Exception e) + { + DebugConsole.ThrowError($"Failed to delete \"${contentFile.Path}\".", e); + } + //RefreshCreateItemFileList(); + RefreshMyItemList(); + return true; + }; + msgBox.Buttons[0].OnClicked += msgBox.Close; + msgBox.Buttons[1].OnClicked = msgBox.Close; return true; - }; - msgBox.Buttons[0].OnClicked += msgBox.Close; - msgBox.Buttons[1].OnClicked = msgBox.Close; - return true; - } - }; + } + }; + } content.Recalculate(); fileFrame.RectTransform.MinSize = diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 7de1726b2..38bc27a7a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -4,17 +4,23 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; -using System.IO; using System.Linq; +using System.Threading; using System.Xml.Linq; -using EventInput; using Microsoft.Xna.Framework.Input; +#if DEBUG +using System.IO; +#else +using Barotrauma.IO; +#endif + +// ReSharper disable AccessToModifiedClosure, PossibleLossOfFraction, RedundantLambdaParameterType, UnusedVariable namespace Barotrauma { class SubEditorScreen : Screen { - private static readonly string[] crewExperienceLevels = new string[] + private static readonly string[] crewExperienceLevels = { "CrewExperienceLow", "CrewExperienceMid", @@ -24,13 +30,15 @@ namespace Barotrauma public enum Mode { Default, - Character, Wiring } + + public static Vector2 MouseDragStart = Vector2.Zero; private readonly Point defaultPreviewImageSize = new Point(640, 368); private readonly Camera cam; + private Vector2 camTargetFocus = Vector2.Zero; private SubmarineInfo backedUpSubInfo; @@ -38,12 +46,16 @@ namespace Barotrauma private bool lightingEnabled; + private bool wasSelectedBefore; + public GUIComponent TopPanel; private GUIComponent showEntitiesPanel, entityCountPanel; - private List showEntitiesTickBoxes = new List(); + private readonly List showEntitiesTickBoxes = new List(); private GUITextBlock subNameLabel; + public bool ShowThalamus { get; private set; } = true; + private bool entityMenuOpen = true; private float entityMenuOpenState = 1.0f; public GUIComponent EntityMenu; @@ -51,7 +63,9 @@ namespace Barotrauma private GUIListBox entityList; private GUIButton toggleEntityMenuButton; - private GUITickBox defaultModeTickBox, wiringModeTickBox, characterModeTickBox; + public GUIButton ToggleEntityMenuButton => toggleEntityMenuButton; + + private GUITickBox defaultModeTickBox, wiringModeTickBox; private GUIComponent loadFrame, saveFrame; @@ -74,12 +88,28 @@ namespace Barotrauma //a Character used for picking up and manipulating items private Character dummyCharacter; + + /// + /// Prefab used for dragging from the item catalog into inventories + /// + /// + public static MapEntityPrefab DraggedItemPrefab; + + /// + /// Currently opened hand-held item container like crates + /// + private Item OpenedItem; + + /// + /// When opening an item we save the location of it so we can teleport the dummy character there + /// + private Vector2 oldItemPosition; private GUIFrame wiringToolPanel; private DateTime editorSelectedTime; - private readonly string containerDeleteTag = "containerdelete"; + private const string containerDeleteTag = "containerdelete"; private GUIImage previewImage; @@ -92,28 +122,29 @@ namespace Barotrauma private GUITextBlock submarineDescriptionCharacterCount; private Mode mode; + + private Color backgroundColor = GameSettings.SubEditorBackgroundColor; // Prevent the mode from changing private bool lockMode; - public override Camera Cam - { - get { return cam; } - } + private static bool isAutoSaving; - public string GetSubDescription() + public override Camera Cam => cam; + + private static string GetSubDescription() { string localizedDescription = TextManager.Get("submarine.description." + (Submarine.MainSub?.Info.Name ?? ""), true); if (localizedDescription != null) { return localizedDescription; } return (Submarine.MainSub == null) ? "" : Submarine.MainSub.Info.Description; } - private string GetTotalHullVolume() + private static string GetTotalHullVolume() { return TextManager.Get("TotalHullVolume") + ":\n" + Hull.hullList.Sum(h => h.Volume); } - private string GetSelectedHullVolume() + private static string GetSelectedHullVolume() { float buoyancyVol = 0.0f; float selectedVol = 0.0f; @@ -142,9 +173,7 @@ namespace Barotrauma return retVal; } - public bool CharacterMode { get { return mode == Mode.Character; } } - - public bool WiringMode { get { return mode == Mode.Wiring; } } + public bool WiringMode => mode == Mode.Wiring; public SubEditorScreen() { @@ -158,7 +187,7 @@ namespace Barotrauma private void CreateUI() { - TopPanel = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), GUI.Canvas) { MinSize = new Point(0, 35) }, "GUIFrameTop"); + TopPanel = new GUIFrame(new RectTransform(new Vector2(GUI.Canvas.RelativeSize.X, 0.01f), GUI.Canvas) { MinSize = new Point(0, 35) }, "GUIFrameTop"); GUILayoutGroup paddedTopPanel = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.8f), TopPanel.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) @@ -171,7 +200,7 @@ namespace Barotrauma ToolTip = TextManager.Get("back"), OnClicked = (b, d) => { - var msgBox = new GUIMessageBox("", TextManager.Get("PauseMenuQuitVerificationEditor"), new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }) + var msgBox = new GUIMessageBox("", TextManager.Get("PauseMenuQuitVerificationEditor"), new[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }) { UserData = "verificationprompt" }; @@ -196,7 +225,7 @@ namespace Barotrauma new GUIButton(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "OpenButton") { ToolTip = TextManager.Get("OpenSubButton"), - OnClicked = (GUIButton btn, object data) => + OnClicked = (btn, data) => { saveFrame = null; CreateLoadScreen(); @@ -209,8 +238,8 @@ namespace Barotrauma new GUIButton(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "SaveButton") { - ToolTip = TextManager.Get("SaveSubButton"), - OnClicked = (GUIButton btn, object data) => + ToolTip = TextManager.Get("SaveSubButton") + "‖color:125,125,125‖\nCtrl + S‖color:end‖", + OnClicked = (btn, data) => { loadFrame = null; CreateSaveScreen(); @@ -279,8 +308,8 @@ namespace Barotrauma defaultModeTickBox = new GUITickBox(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "EditSubButton") { - ToolTip = TextManager.Get("SubEditorEditingMode"), - OnSelected = (GUITickBox tBox) => + ToolTip = TextManager.Get("SubEditorEditingMode") + "‖color:125,125,125‖\nCtrl + 1‖color:end‖", + OnSelected = tBox => { if (!lockMode) { @@ -288,35 +317,23 @@ namespace Barotrauma return true; } - else { return false; } - } - }; - characterModeTickBox = new GUITickBox(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "CharacterModeButton") - { - ToolTip = TextManager.Get("CharacterModeButton") + '\n' + TextManager.Get("CharacterModeToolTip"), - OnSelected = (GUITickBox tBox) => - { - if (!lockMode) - { - SetMode(tBox.Selected ? Mode.Character : Mode.Default); - return true; - } - else { return false; } + return false; } }; wiringModeTickBox = new GUITickBox(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "WiringModeButton") { - ToolTip = TextManager.Get("WiringModeButton") + '\n' + TextManager.Get("WiringModeToolTip"), - OnSelected = (GUITickBox tBox) => + ToolTip = TextManager.Get("WiringModeButton") + '\n' + TextManager.Get("WiringModeToolTip") + "‖color:125,125,125‖\nCtrl + 2‖color:end‖", + OnSelected = tBox => { if (!lockMode) { SetMode(tBox.Selected ? Mode.Wiring : Mode.Default); return true; } - else { return false; } + + return false; } }; @@ -330,8 +347,8 @@ namespace Barotrauma { if (WayPoint.WayPointList.Any()) { - var generateWaypointsVerification = new GUIMessageBox("", TextManager.Get("generatewaypointsverification"), new string[] { TextManager.Get("ok"), TextManager.Get("cancel") }); - generateWaypointsVerification.Buttons[0].OnClicked = (btn, userdata) => + var generateWaypointsVerification = new GUIMessageBox("", TextManager.Get("generatewaypointsverification"), new[] { TextManager.Get("ok"), TextManager.Get("cancel") }); + generateWaypointsVerification.Buttons[0].OnClicked = delegate { if (GenerateWaypoints()) { @@ -368,7 +385,7 @@ namespace Barotrauma //----------------------------------------------- - previouslyUsedPanel = new GUIFrame(new RectTransform(new Vector2(0.1f, 0.2f), GUI.Canvas, Anchor.TopLeft) { MinSize = new Point(200, 200) }) + previouslyUsedPanel = new GUIFrame(new RectTransform(new Vector2(0.1f, 0.2f), GUI.Canvas) { MinSize = new Point(200, 200) }) { Visible = false }; @@ -464,6 +481,12 @@ namespace Barotrauma Selected = Gap.ShowGaps, OnSelected = (GUITickBox obj) => { Gap.ShowGaps = obj.Selected; return true; }, }; + new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedShowEntitiesPanel.RectTransform), TextManager.Get("mapentitycategory.thalamus")) + { + UserData = "thalamus", + Selected = ShowThalamus, + OnSelected = (GUITickBox obj) => { ShowThalamus = obj.Selected; return true; }, + }; showEntitiesTickBoxes.AddRange(paddedShowEntitiesPanel.Children.Select(c => c as GUITickBox)); @@ -548,7 +571,7 @@ namespace Barotrauma }; entityCountPanel.RectTransform.NonScaledSize = new Point( - (int)(paddedEntityCountPanel.RectTransform.Children.Max(c => (int)(c.GUIComponent as GUITextBlock).TextSize.X / 0.75f) / paddedEntityCountPanel.RectTransform.RelativeSize.X), + (int)(paddedEntityCountPanel.RectTransform.Children.Max(c => (int)((GUITextBlock) c.GUIComponent).TextSize.X / 0.75f) / paddedEntityCountPanel.RectTransform.RelativeSize.X), (int)(paddedEntityCountPanel.RectTransform.Children.Sum(c => (int)(c.NonScaledSize.Y * 1.5f) + paddedEntityCountPanel.AbsoluteSpacing) / paddedEntityCountPanel.RectTransform.RelativeSize.Y)); //GUITextBlock.AutoScaleAndNormalize(paddedEntityCountPanel.Children.Where(c => c is GUITextBlock).Cast()); @@ -590,6 +613,7 @@ namespace Barotrauma toggleEntityMenuButton = new GUIButton(new RectTransform(new Vector2(0.15f, 0.08f), EntityMenu.RectTransform, Anchor.TopCenter, Pivot.BottomCenter) { MinSize = new Point(0, 15) }, style: "UIToggleButtonVertical") { + ToolTip = TextManager.Get("EntityMenuToggleTooltip") + "‖color:125,125,125‖\nQ‖color:end‖", OnClicked = (btn, userdata) => { entityMenuOpen = !entityMenuOpen; @@ -633,7 +657,6 @@ namespace Barotrauma { OnClicked = (btn, userdata) => { - if (!string.IsNullOrEmpty(entityFilterBox.Text)) { ClearFilter(); } OpenEntityMenu(null); return true; } @@ -648,7 +671,6 @@ namespace Barotrauma ToolTip = TextManager.Get("MapEntityCategory." + category.ToString()), OnClicked = (btn, userdata) => { - if (!string.IsNullOrEmpty(entityFilterBox.Text)) { ClearFilter(); } MapEntityCategory newCategory = (MapEntityCategory)userdata; OpenEntityMenu(newCategory); return true; @@ -659,7 +681,7 @@ namespace Barotrauma new GUIFrame(new RectTransform(new Vector2(0.8f, 0.01f), paddedTab.RectTransform), style: "HorizontalLine"); - entityList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.9f), paddedTab.RectTransform)) + entityList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.9f), paddedTab.RectTransform), useMouseDownToSelect: true) { OnSelected = SelectPrefab, UseGridLayout = true, @@ -729,10 +751,15 @@ namespace Barotrauma frame.RectTransform.MinSize = new Point(0, frame.Rect.Width); frame.RectTransform.MaxSize = new Point(int.MaxValue, frame.Rect.Width); - - string name = legacy ? ep.Name + " (legacy)" : ep.Name; + string name = legacy ? TextManager.GetWithVariable("legacyitemformat", "[name]", ep.Name) : ep.Name; frame.ToolTip = string.IsNullOrEmpty(ep.Description) ? name : name + '\n' + ep.Description; + if (ep.HideInMenus) + { + frame.Color = Color.Red; + name = "[HIDDEN] " + name; + } + GUILayoutGroup paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.8f), frame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) { Stretch = true, @@ -760,7 +787,8 @@ namespace Barotrauma img = new GUIImage(new RectTransform(new Vector2(1.0f, 0.8f), paddedFrame.RectTransform, Anchor.TopCenter), icon) { - CanBeFocused = false, + CanBeFocused = false, + LoadAsynchronously = true, Color = legacy ? iconColor * 0.6f : iconColor }; } @@ -768,7 +796,11 @@ namespace Barotrauma if (ep is ItemAssemblyPrefab itemAssemblyPrefab) { new GUICustomComponent(new RectTransform(new Vector2(1.0f, 0.75f), - paddedFrame.RectTransform, Anchor.TopCenter), onDraw: itemAssemblyPrefab.DrawIcon, onUpdate: null) + paddedFrame.RectTransform, Anchor.TopCenter), onDraw: (sb, customComponent) => + { + if (GUIImage.LoadingTextures) { return; } + itemAssemblyPrefab.DrawIcon(sb, customComponent); + }) { HideElementsOutsideFrame = true, ToolTip = frame.RawToolTip @@ -776,7 +808,7 @@ namespace Barotrauma } GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform, Anchor.BottomCenter), - text: ep.Name, textAlignment: Alignment.Center, font: GUI.SmallFont) + text: name, textAlignment: Alignment.Center, font: GUI.SmallFont) { CanBeFocused = false }; @@ -791,27 +823,30 @@ namespace Barotrauma UserData = ep, OnClicked = (btn, userData) => { - ItemAssemblyPrefab assemblyPrefab = userData as ItemAssemblyPrefab; - var msgBox = new GUIMessageBox( - TextManager.Get("DeleteDialogLabel"), - TextManager.GetWithVariable("DeleteDialogQuestion", "[file]", assemblyPrefab.Name), - new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); - msgBox.Buttons[0].OnClicked += (deleteBtn, userData2) => - { - try + ItemAssemblyPrefab assemblyPrefab = (ItemAssemblyPrefab) userData; + if (assemblyPrefab != null) { + var msgBox = new GUIMessageBox( + TextManager.Get("DeleteDialogLabel"), + TextManager.GetWithVariable("DeleteDialogQuestion", "[file]", assemblyPrefab.Name), + new[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); + msgBox.Buttons[0].OnClicked += (deleteBtn, userData2) => { - assemblyPrefab.Delete(); - UpdateEntityList(); - OpenEntityMenu(MapEntityCategory.ItemAssembly); - } - catch (Exception e) - { - DebugConsole.ThrowError(TextManager.GetWithVariable("DeleteFileError", "[file]", assemblyPrefab.Name), e); - } - return true; - }; - msgBox.Buttons[0].OnClicked += msgBox.Close; - msgBox.Buttons[1].OnClicked += msgBox.Close; + try + { + assemblyPrefab.Delete(); + UpdateEntityList(); + OpenEntityMenu(MapEntityCategory.ItemAssembly); + } + catch (Exception e) + { + DebugConsole.ThrowError(TextManager.GetWithVariable("DeleteFileError", "[file]", assemblyPrefab.Name), e); + } + return true; + }; + msgBox.Buttons[0].OnClicked += msgBox.Close; + msgBox.Buttons[1].OnClicked += msgBox.Close; + } + return true; } }; @@ -824,8 +859,8 @@ namespace Barotrauma } } - entityList.Content.RectTransform.SortChildren((i1, i2) => - (i1.GUIComponent.UserData as MapEntityPrefab).Name.CompareTo((i2.GUIComponent.UserData as MapEntityPrefab).Name)); + entityList.Content.RectTransform.SortChildren((i1, i2) => + string.Compare(((MapEntityPrefab) i1.GUIComponent.UserData). Name, (i2.GUIComponent.UserData as MapEntityPrefab)?.Name, StringComparison.Ordinal)); } public override void Select() @@ -835,10 +870,17 @@ namespace Barotrauma GameMain.LightManager.AmbientLight = Level.Loaded?.GenerationParams?.AmbientLightColor ?? LevelGenerationParams.LevelParams?.FirstOrDefault()?.AmbientLightColor ?? - new Color(20, 20, 20, 255); + new Color(20, 20, 20, 255); UpdateEntityList(); + isAutoSaving = false; + if (!wasSelectedBefore) + { + OpenEntityMenu(null); + wasSelectedBefore = true; + } + if (backedUpSubInfo != null) { Submarine.Unload(); @@ -848,15 +890,6 @@ namespace Barotrauma if (backedUpSubInfo != null) { name = backedUpSubInfo.Name; } subNameLabel.Text = ToolBox.LimitString(name, subNameLabel.Font, subNameLabel.Rect.Width); - foreach (MapEntityPrefab prefab in MapEntityPrefab.List) - { - prefab.sprite?.EnsureLazyLoaded(); - if (prefab is ItemPrefab itemPrefab) - { - itemPrefab.InventoryIcon?.EnsureLazyLoaded(); - } - } - editorSelectedTime = DateTime.Now; GUI.ForceMouseOn(null); @@ -877,8 +910,8 @@ namespace Barotrauma Submarine.MainSub.UpdateTransform(interpolate: false); cam.Position = Submarine.MainSub.Position + Submarine.MainSub.HiddenSubPosition; - GameMain.SoundManager.SetCategoryGainMultiplier("default", 0.0f, 0); - GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", 0.0f, 0); + GameMain.SoundManager.SetCategoryGainMultiplier("default", 0.0f); + GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", 0.0f); linkedSubBox.ClearChildren(); foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) @@ -888,6 +921,13 @@ namespace Barotrauma cam.UpdateTransform(); + CreateDummyCharacter(); + + if (GameSettings.EnableSubmarineAutoSave) + { + CoroutineManager.StartCoroutine(AutoSaveCoroutine(), "SubEditorAutoSave"); + } + GameAnalyticsManager.SetCustomDimension01("editor"); if (!GameMain.Config.EditorDisclaimerShown) { @@ -895,6 +935,43 @@ namespace Barotrauma } } + /// + /// Coroutine that waits 5 minutes and then runs itself recursively again to save the submarine into a temporary file + /// + /// + /// + private static IEnumerable AutoSaveCoroutine() + { + DateTime target = DateTime.Now.AddMinutes(5); + DateTime tempTarget = DateTime.Now; + + bool wasPaused = false; + + while (DateTime.Now < target && Selected is SubEditorScreen || GameMain.Instance.Paused || wasPaused) + { + if (GameMain.Instance.Paused && !wasPaused) + { + AutoSave(); + tempTarget = DateTime.Now; + wasPaused = true; + } + + if (!GameMain.Instance.Paused && wasPaused) + { + wasPaused = false; + target = target.AddSeconds((DateTime.Now - tempTarget).TotalSeconds); + } + yield return CoroutineStatus.Running; + } + + if (Selected is SubEditorScreen) + { + AutoSave(); + CoroutineManager.StartCoroutine(AutoSaveCoroutine(), "SubEditorAutoSave"); + } + yield return CoroutineStatus.Success; + } + public override void Deselect() { base.Deselect(); @@ -917,8 +994,13 @@ namespace Barotrauma SetMode(Mode.Default); SoundPlayer.OverrideMusicType = null; - GameMain.SoundManager.SetCategoryGainMultiplier("default", GameMain.Config.SoundVolume, 0); - GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", GameMain.Config.SoundVolume, 0); + GameMain.SoundManager.SetCategoryGainMultiplier("default", GameMain.Config.SoundVolume); + GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", GameMain.Config.SoundVolume); + + if (CoroutineManager.IsCoroutineRunning("SubEditorAutoSave")) + { + CoroutineManager.StopCoroutines("SubEditorAutoSave"); + } if (dummyCharacter != null) { @@ -927,13 +1009,13 @@ namespace Barotrauma GameMain.World.ProcessChanges(); } - if (GUIMessageBox.MessageBoxes.Any(mbox => (mbox as GUIMessageBox).Tag == containerDeleteTag)) + if (GUIMessageBox.MessageBoxes.Any(mbox => (mbox as GUIMessageBox)?.Tag == containerDeleteTag)) { for (int i = 0; i < GUIMessageBox.MessageBoxes.Count; i++) { GUIMessageBox box = GUIMessageBox.MessageBoxes[i] as GUIMessageBox; - if (box.Tag != containerDeleteTag) continue; - box.Close(); + if (box != null && box.Tag != containerDeleteTag) continue; + box?.Close(); i--; // Take into account the message boxes removing themselves from the list when closed } } @@ -953,9 +1035,9 @@ namespace Barotrauma if (itemNames.Length > 0) { // Multiple prompts open - if (GUIMessageBox.MessageBoxes.Any(mbox => (mbox as GUIMessageBox).Tag == containerDeleteTag)) + if (GUIMessageBox.MessageBoxes.Any(mbox => (mbox as GUIMessageBox)?.Tag == containerDeleteTag)) { - var msgBox = new GUIMessageBox(itemToDelete.Name, TextManager.Get("DeletingContainerWithItems") + itemNames, new string[] { TextManager.Get("Yes"), TextManager.Get("No"), TextManager.Get("YesToAll"), TextManager.Get("NoToAll") }, tag: containerDeleteTag); + var msgBox = new GUIMessageBox(itemToDelete.Name, TextManager.Get("DeletingContainerWithItems") + itemNames, new[] { TextManager.Get("Yes"), TextManager.Get("No"), TextManager.Get("YesToAll"), TextManager.Get("NoToAll") }, tag: containerDeleteTag); // Yes msgBox.Buttons[0].OnClicked = (btn, userdata) => @@ -990,9 +1072,9 @@ namespace Barotrauma for (int i = 0; i < GUIMessageBox.MessageBoxes.Count; i++) { GUIMessageBox box = GUIMessageBox.MessageBoxes[i] as GUIMessageBox; - if (box.Tag != msgBox.Tag || box == msgBox) continue; - GUIButton button = box.Buttons[0]; - button.OnClicked(button, button.UserData); + if (box?.Tag != msgBox.Tag || box == msgBox) continue; + GUIButton button = box?.Buttons[0]; + button?.OnClicked(button, button.UserData); i--; // Take into account the message boxes removing themselves from the list when closed } @@ -1007,9 +1089,9 @@ namespace Barotrauma for (int i = 0; i < GUIMessageBox.MessageBoxes.Count; i++) { GUIMessageBox box = GUIMessageBox.MessageBoxes[i] as GUIMessageBox; - if (box.Tag != msgBox.Tag || box == msgBox) continue; - GUIButton button = box.Buttons[1]; - button.OnClicked(button, button.UserData); + if (box?.Tag != msgBox.Tag || box == msgBox) continue; + GUIButton button = box?.Buttons[1]; + button?.OnClicked(button, button.UserData); i--; // Take into account the message boxes removing themselves from the list when closed } @@ -1031,7 +1113,7 @@ namespace Barotrauma } else // Single prompt { - var msgBox = new GUIMessageBox(itemToDelete.Name, TextManager.Get("DeletingContainerWithItems") + itemNames, new string[] { TextManager.Get("Yes"), TextManager.Get("No") }, tag: containerDeleteTag); + var msgBox = new GUIMessageBox(itemToDelete.Name, TextManager.Get("DeletingContainerWithItems") + itemNames, new[] { TextManager.Get("Yes"), TextManager.Get("No") }, tag: containerDeleteTag); // Yes msgBox.Buttons[0].OnClicked = (btn, userdata) => @@ -1084,6 +1166,46 @@ namespace Barotrauma GameMain.World.ProcessChanges(); } + /// + /// Saves the current main sub into a temporary file outside of the Submarines/ folder + /// + /// + /// The saving is ran in another thread to avoid lag spikes + private static void AutoSave() + { + if (MapEntity.mapEntityList.Any() && GameSettings.EnableSubmarineAutoSave && !isAutoSaving) + { + if (Submarine.MainSub != null) + { + isAutoSaving = true; + string filePath = Path.Combine(SubmarineInfo.SavePath, ".AutoSaves"); + if (!Directory.Exists(filePath)) + { + var e = Directory.CreateDirectory(filePath); + e.Attributes = System.IO.FileAttributes.Directory | System.IO.FileAttributes.Hidden; + if (!e.Exists) { return; } + } + + XDocument doc = new XDocument(new XElement("Submarine")); + Submarine.MainSub.SaveToXElement(doc.Root); + Thread saveThread = new Thread(start => + { + try + { + SaveUtil.CompressStringToFile(Path.Combine(filePath, "AutoSave.sub"), doc.ToString()); + CrossThread.RequestExecutionOnMainThread(() => GUI.AddMessage(TextManager.Get("AutoSaved"), GUI.Style.Green, playSound: false)); + } + catch (Exception e) + { + CrossThread.RequestExecutionOnMainThread(() => DebugConsole.ThrowError("Saving submarine \"" + filePath + "\" failed!", e)); + } + isAutoSaving = false; + }) { Name = "Auto Save Thread" }; + saveThread.Start(); + } + } + } + private bool SaveSub(GUIButton button, object obj) { if (string.IsNullOrWhiteSpace(nameBox.Text)) @@ -1111,14 +1233,35 @@ namespace Barotrauma GUI.AddMessage(TextManager.GetWithVariable("SubNameIllegalCharsWarning", "[illegalchar]", illegalChar.ToString()), GUI.Style.Red); return false; } - + + Submarine.MainSub.Info.Name = name; + string savePath = name + ".sub"; string prevSavePath = null; if (!string.IsNullOrEmpty(Submarine.MainSub?.Info.FilePath) && Submarine.MainSub.Info.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) { prevSavePath = Submarine.MainSub.Info.FilePath.CleanUpPath(); - savePath = Path.Combine(Path.GetDirectoryName(Submarine.MainSub.Info.FilePath), savePath).CleanUpPath(); + string prevDir = Path.GetDirectoryName(Submarine.MainSub.Info.FilePath).CleanUpPath(); + string[] subDirs = prevDir.Split('/'); + bool forceToSubFolder = Steam.SteamManager.IsInitialized; + if (forceToSubFolder && subDirs.Length>1 && subDirs[0].Equals("Mods", StringComparison.InvariantCultureIgnoreCase)) + { + string modName = subDirs[1]; + ContentPackage contentPackage = ContentPackage.List.Find(p => p.Name.Equals(modName, StringComparison.InvariantCultureIgnoreCase)); + if (contentPackage != null) + { + Steamworks.Data.PublishedFileId packageId = Steam.SteamManager.GetWorkshopItemIDFromUrl(contentPackage.SteamWorkshopUrl); + Steamworks.Ugc.Item? item = Steamworks.Ugc.Item.GetAsync(packageId).Result; + if (item?.Owner.Id == Steam.SteamManager.GetSteamID()) + { + forceToSubFolder = false; + contentPackage.Files.Add(new ContentFile(Path.Combine(prevDir, savePath).CleanUpPath(), ContentType.Submarine)); + contentPackage.Save(contentPackage.Path); + } + } + } + savePath = Path.Combine(forceToSubFolder ? SubmarineInfo.SavePath : prevDir, savePath).CleanUpPath(); } else { @@ -1139,47 +1282,61 @@ namespace Barotrauma } #endif - if (previewImage?.Sprite?.Texture != null) + if (Submarine.MainSub != null) { - using (MemoryStream imgStream = new MemoryStream()) + if (previewImage?.Sprite?.Texture != null) { - previewImage.Sprite.Texture.SaveAsPng(imgStream, previewImage.Sprite.Texture.Width, previewImage.Sprite.Texture.Height); - Submarine.MainSub.SaveAs(savePath, imgStream); + bool savePreviewImage = true; + using System.IO.MemoryStream imgStream = new System.IO.MemoryStream(); + try + { + previewImage.Sprite.Texture.SaveAsPng(imgStream, previewImage.Sprite.Texture.Width, previewImage.Sprite.Texture.Height); + } + catch (Exception e) + { + DebugConsole.ThrowError($"Saving the preview image of the submarine \"{Submarine.MainSub.Info.Name}\" failed.", e); + savePreviewImage = false; + } + Submarine.MainSub.SaveAs(savePath, savePreviewImage ? imgStream : null); } - } - else - { - Submarine.MainSub.SaveAs(savePath); - } - Submarine.MainSub?.CheckForErrors(); - - GUI.AddMessage(TextManager.GetWithVariable("SubSavedNotification", "[filepath]", savePath), GUI.Style.Green); - SubmarineInfo.RefreshSavedSub(savePath); - if (prevSavePath != null && prevSavePath != savePath) - { - SubmarineInfo.RefreshSavedSub(prevSavePath); - } + else + { + Submarine.MainSub.SaveAs(savePath); + } + + Submarine.MainSub.CheckForErrors(); + + GUI.AddMessage(TextManager.GetWithVariable("SubSavedNotification", "[filepath]", savePath), GUI.Style.Green); - linkedSubBox.ClearChildren(); - foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) - { - linkedSubBox.AddItem(sub.Name, sub); - } + SubmarineInfo.RefreshSavedSub(savePath); + if (prevSavePath != null && prevSavePath != savePath) { SubmarineInfo.RefreshSavedSub(prevSavePath); } - subNameLabel.Text = ToolBox.LimitString(Submarine.MainSub.Info.Name, subNameLabel.Font, subNameLabel.Rect.Width); + linkedSubBox.ClearChildren(); + foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) { linkedSubBox.AddItem(sub.Name, sub); } + + subNameLabel.Text = ToolBox.LimitString(Submarine.MainSub.Info.Name, subNameLabel.Font, subNameLabel.Rect.Width); + } return false; } - private void CreateSaveScreen() + private void CreateSaveScreen(bool quickSave = false) { - SetMode(Mode.Default); + if (saveFrame != null) { return; } + + if (!quickSave) + { + CloseItem(); + SetMode(Mode.Default); + } - saveFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas), style: "GUIBackgroundBlocker") + saveFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null) { OnClicked = (btn, userdata) => { if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) saveFrame = null; return true; } }; + new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, saveFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); + var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.4f, 0.5f), saveFrame.RectTransform, Anchor.Center) { MinSize = new Point(750, 400) }); var paddedSaveFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), innerFrame.RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.02f }; @@ -1342,10 +1499,10 @@ namespace Barotrauma { OnClicked = (btn, userdata) => { - using (MemoryStream imgStream = new MemoryStream()) + using (System.IO.MemoryStream imgStream = new System.IO.MemoryStream()) { CreateImage(defaultPreviewImageSize.X, defaultPreviewImageSize.Y, imgStream); - previewImage.Sprite = new Sprite(TextureLoader.FromStream(imgStream), null, null); + previewImage.Sprite = new Sprite(TextureLoader.FromStream(imgStream, compress: false), null, null); if (Submarine.MainSub != null) { Submarine.MainSub.Info.PreviewImage = previewImage.Sprite; @@ -1400,7 +1557,7 @@ namespace Barotrauma var tagTickBox = new GUITickBox(new RectTransform(new Vector2(0.2f, 0.2f), tagContainer.Content.RectTransform), tagStr, font: GUI.SmallFont) { - Selected = Submarine.MainSub == null ? false : Submarine.MainSub.Info.HasTag(tag), + Selected = Submarine.MainSub != null && Submarine.MainSub.Info.HasTag(tag), UserData = tag, OnSelected = (GUITickBox tickBox) => @@ -1425,34 +1582,36 @@ namespace Barotrauma var contentPackList = new GUIListBox(new RectTransform(new Vector2(0.5f, 1.0f - contentPackagesLabel.RectTransform.RelativeSize.Y), horizontalArea.RectTransform, Anchor.BottomRight)); - List contentPacks = Submarine.MainSub.Info.RequiredContentPackages.ToList(); - foreach (ContentPackage contentPack in ContentPackage.List) - { - //don't show content packages that only define submarine files - //(it doesn't make sense to require another sub to be installed to install this one) - if (contentPack.Files.All(cp => cp.Type == ContentType.Submarine)) { continue; } - if (!contentPacks.Contains(contentPack.Name)) { contentPacks.Add(contentPack.Name); } - } + if (Submarine.MainSub != null) { + List contentPacks = Submarine.MainSub.Info.RequiredContentPackages.ToList(); + foreach (ContentPackage contentPack in ContentPackage.List) + { + //don't show content packages that only define submarine files + //(it doesn't make sense to require another sub to be installed to install this one) + if (contentPack.Files.All(cp => cp.Type == ContentType.Submarine)) { continue; } + if (!contentPacks.Contains(contentPack.Name)) { contentPacks.Add(contentPack.Name); } + } - foreach (string contentPackageName in contentPacks) - { - var cpTickBox = new GUITickBox(new RectTransform(new Vector2(0.2f, 0.2f), contentPackList.Content.RectTransform), contentPackageName, font: GUI.SmallFont) + foreach (string contentPackageName in contentPacks) { - Selected = Submarine.MainSub.Info.RequiredContentPackages.Contains(contentPackageName), - UserData = contentPackageName - }; - cpTickBox.OnSelected += (GUITickBox tickBox) => - { - if (tickBox.Selected) + var cpTickBox = new GUITickBox(new RectTransform(new Vector2(0.2f, 0.2f), contentPackList.Content.RectTransform), contentPackageName, font: GUI.SmallFont) { - Submarine.MainSub.Info.RequiredContentPackages.Add((string)tickBox.UserData); - } - else + Selected = Submarine.MainSub.Info.RequiredContentPackages.Contains(contentPackageName), + UserData = contentPackageName + }; + cpTickBox.OnSelected += tickBox => { - Submarine.MainSub.Info.RequiredContentPackages.Remove((string)tickBox.UserData); - } - return true; - }; + if (tickBox.Selected) + { + Submarine.MainSub.Info.RequiredContentPackages.Add((string)tickBox.UserData); + } + else + { + Submarine.MainSub.Info.RequiredContentPackages.Remove((string)tickBox.UserData); + } + return true; + }; + } } @@ -1477,6 +1636,8 @@ namespace Barotrauma leftColumn.Recalculate(); descriptionBox.Text = Submarine.MainSub == null ? "" : Submarine.MainSub.Info.Description; submarineDescriptionCharacterCount.Text = descriptionBox.Text.Length + " / " + submarineDescriptionLimit; + + if (quickSave) { SaveSub(saveButton, saveButton.UserData); } } @@ -1484,11 +1645,13 @@ namespace Barotrauma { SetMode(Mode.Default); - saveFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas), style: "GUIBackgroundBlocker") + saveFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null) { OnClicked = (btn, userdata) => { if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) saveFrame = null; return true; } }; + new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, saveFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); + var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.25f, 0.3f), saveFrame.RectTransform, Anchor.Center) { MinSize = new Point(400, 300) }); GUILayoutGroup paddedSaveFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), innerFrame.RectTransform, Anchor.Center)) { @@ -1535,6 +1698,27 @@ namespace Barotrauma }; } + /// + /// Loads an item assembly and only returns items which are not inside other inventories. + /// This is to prevent us from trying to place for example Oxygen Tanks inside an inventory + /// when it's already inside a diving suit. + /// + /// + /// + private List LoadItemAssemblyInventorySafe(ItemAssemblyPrefab assemblyPrefab) + { + var realItems = assemblyPrefab.CreateInstance(Vector2.Zero, Submarine.MainSub); + var itemInstance = new List(); + realItems.ForEach(entity => + { + if (entity is Item it && it.ParentInventory == null) + { + itemInstance.Add(it); + } + }); + return itemInstance; + } + private bool SaveAssembly(GUIButton button, object obj) { if (string.IsNullOrWhiteSpace(nameBox.Text)) @@ -1563,7 +1747,7 @@ namespace Barotrauma if (File.Exists(filePath)) { - var msgBox = new GUIMessageBox(TextManager.Get("Warning"), TextManager.Get("ItemAssemblyFileExistsWarning"), new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); + var msgBox = new GUIMessageBox(TextManager.Get("Warning"), TextManager.Get("ItemAssemblyFileExistsWarning"), new[] { TextManager.Get("Yes"), TextManager.Get("No") }); msgBox.Buttons[0].OnClicked = (btn, userdata) => { msgBox.Close(); @@ -1581,7 +1765,11 @@ namespace Barotrauma void Save() { XDocument doc = new XDocument(ItemAssemblyPrefab.Save(MapEntity.SelectedList, nameBox.Text, descriptionBox.Text, hideInMenus)); +#if DEBUG doc.Save(filePath); +#else + doc.SaveSafe(filePath); +#endif new ItemAssemblyPrefab(filePath); UpdateEntityList(); @@ -1591,20 +1779,27 @@ namespace Barotrauma return false; } - private bool CreateLoadScreen() + private void CreateLoadScreen() { + CloseItem(); SetMode(Mode.Default); - loadFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas), style: "GUIBackgroundBlocker") + loadFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null) { OnClicked = (btn, userdata) => { if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) loadFrame = null; return true; }, }; - var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.2f, 0.36f), loadFrame.RectTransform, Anchor.Center) { MinSize = new Point(350, 500) }); + new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, loadFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); + + var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.5f), loadFrame.RectTransform, Anchor.Center) { MinSize = new Point(350, 500) }); var paddedLoadFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), innerFrame.RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.02f }; - var deleteButtonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), paddedLoadFrame.RectTransform, Anchor.Center)); + var deleteButtonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.2f), paddedLoadFrame.RectTransform, Anchor.Center)) + { + RelativeSpacing = 0.1f, + Stretch = true + }; var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedLoadFrame.RectTransform), font: GUI.Font, createClearButton: true); var searchTitle = new GUITextBlock(new RectTransform(Vector2.One, searchBox.RectTransform), TextManager.Get("serverlog.filter"), @@ -1615,7 +1810,7 @@ namespace Barotrauma }; searchTitle.TextColor *= 0.5f; - var subList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), paddedLoadFrame.RectTransform)) + var subList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.7f), paddedLoadFrame.RectTransform)) { ScrollBarVisible = true, OnSelected = (GUIComponent selected, object userData) => @@ -1625,7 +1820,7 @@ namespace Barotrauma #if DEBUG deleteBtn.Enabled = true; #else - deleteBtn.Enabled = userData is Submarine sub && !sub.Info.IsVanillaSubmarine(); + deleteBtn.Enabled = userData is SubmarineInfo subInfo && !subInfo.IsVanillaSubmarine(); #endif } return true; @@ -1656,7 +1851,7 @@ namespace Barotrauma } } - var deleteButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), deleteButtonHolder.RectTransform, Anchor.TopCenter), + var deleteButton = new GUIButton(new RectTransform(Vector2.One, deleteButtonHolder.RectTransform, Anchor.TopCenter), TextManager.Get("Delete")) { Enabled = false, @@ -1671,6 +1866,18 @@ namespace Barotrauma deleteButton.Enabled = false; return true; }; + + var loadAutoSave = new GUIButton(new RectTransform(Vector2.One, deleteButtonHolder.RectTransform, Anchor.BottomCenter), TextManager.Get("LoadAutoSave")) + { + Enabled = File.Exists(Path.Combine(SubmarineInfo.SavePath, ".AutoSaves", "AutoSave.sub")), + ToolTip = TextManager.Get("LoadAutoSaveTooltip"), + UserData = "loadautosave", + OnClicked = (button, o) => + { + LoadAutoSave(); + return true; + } + }; var controlBtnHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), paddedLoadFrame.RectTransform), isHorizontal: true) { RelativeSpacing = 0.2f, Stretch = true }; @@ -1689,16 +1896,58 @@ namespace Barotrauma { OnClicked = LoadSub }; - - return true; } private void FilterSubs(GUIListBox subList, string filter) { foreach (GUIComponent child in subList.Content.Children) { - if (!(child.UserData is Submarine sub)) { return; } - child.Visible = string.IsNullOrEmpty(filter) ? true : sub.Info.Name.ToLower().Contains(filter.ToLower()); + if (!(child.UserData is SubmarineInfo sub)) { return; } + child.Visible = string.IsNullOrEmpty(filter) || sub.Name.ToLower().Contains(filter.ToLower()); + } + } + + /// + /// Recovers the auto saved submarine + /// + /// + private void LoadAutoSave() + { + string filePath = Path.Combine(SubmarineInfo.SavePath, ".AutoSaves", "AutoSave.sub"); + + var loadedSub = Submarine.Load(new SubmarineInfo(filePath), true); + + // set the submarine file path to the "default" value + loadedSub.Info.FilePath = Path.Combine(SubmarineInfo.SavePath, $"{TextManager.Get("UnspecifiedSubFileName")}.sub"); + loadedSub.Info.Name = TextManager.Get("UnspecifiedSubFileName"); + try + { + loadedSub.Info.Name = loadedSub.Info.SubmarineElement.GetAttributeString("name", loadedSub.Info.Name); + } + catch (Exception e) + { + DebugConsole.ThrowError("Failed to find a name for the submarine.", e); + } + Submarine.MainSub = loadedSub; + Submarine.MainSub.SetPrevTransform(Submarine.MainSub.Position); + Submarine.MainSub.UpdateTransform(); + Submarine.MainSub.Info.Name = loadedSub.Info.Name; + subNameLabel.Text = ToolBox.LimitString(loadedSub.Info.Name, subNameLabel.Font, subNameLabel.Rect.Width); + + CreateDummyCharacter(); + + cam.Position = Submarine.MainSub.Position + Submarine.MainSub.HiddenSubPosition; + + loadFrame = null; + + //turn off lights that are inside an inventory (cabinet for example) + foreach (Item item in Item.ItemList) + { + var lightComponent = item.GetComponent(); + if (lightComponent != null) + { + lightComponent.Light.Enabled = item.ParentInventory == null; + } } } @@ -1724,6 +1973,8 @@ namespace Barotrauma var selectedSub = new Submarine(selectedSubInfo); Submarine.MainSub = selectedSub; Submarine.MainSub.UpdateTransform(interpolate: false); + + CreateDummyCharacter(); string name = Submarine.MainSub.Info.Name; subNameLabel.Text = ToolBox.LimitString(name, subNameLabel.Font, subNameLabel.Rect.Width); @@ -1745,7 +1996,7 @@ namespace Barotrauma if (selectedSub.Info.GameVersion < new Version("0.8.9.0")) { var adjustLightsPrompt = new GUIMessageBox(TextManager.Get("Warning"), TextManager.Get("AdjustLightsPrompt"), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); + new[] { TextManager.Get("Yes"), TextManager.Get("No") }); adjustLightsPrompt.Buttons[0].OnClicked += adjustLightsPrompt.Close; adjustLightsPrompt.Buttons[0].OnClicked += (btn, userdata) => { @@ -1804,19 +2055,19 @@ namespace Barotrauma msgBox.Buttons[1].OnClicked += msgBox.Close; } - private bool OpenEntityMenu(MapEntityCategory? selectedCategory) + private void OpenEntityMenu(MapEntityCategory? entityCategory) { foreach (GUIButton categoryButton in entityCategoryButtons) { - categoryButton.Selected = selectedCategory.HasValue ? - categoryButton.UserData is MapEntityCategory category && selectedCategory.Value == category : + categoryButton.Selected = entityCategory.HasValue ? + categoryButton.UserData is MapEntityCategory category && entityCategory.Value == category : categoryButton.UserData == null; - string categoryName = selectedCategory.HasValue ? selectedCategory.Value.ToString() : "All"; + string categoryName = entityCategory.HasValue ? entityCategory.Value.ToString() : "All"; selectedCategoryText.Text = TextManager.Get("MapEntityCategory." + categoryName); selectedCategoryButton.ApplyStyle(GUI.Style.GetComponentStyle("CategoryButton." + categoryName)); } - this.selectedCategory = selectedCategory; + selectedCategory = entityCategory; SetMode(Mode.Default); @@ -1830,43 +2081,60 @@ namespace Barotrauma foreach (GUIComponent child in entityList.Content.Children) { - child.Visible = !selectedCategory.HasValue || ((MapEntityPrefab)child.UserData).Category == selectedCategory; + child.Visible = !entityCategory.HasValue || ((MapEntityPrefab) child.UserData).Category == entityCategory; + if (child.Visible && dummyCharacter?.SelectedConstruction?.OwnInventory != null) + { + child.Visible = child.UserData is MapEntityPrefab item && IsItemPrefab(item); + } } + + if (!string.IsNullOrEmpty(entityFilterBox.Text)) { FilterEntities(entityFilterBox.Text); } + entityList.UpdateScrollBarSize(); entityList.BarScroll = 0.0f; - - return true; } - private bool FilterEntities(string filter) + private void FilterEntities(string filter) { if (string.IsNullOrWhiteSpace(filter)) { - entityList.Content.Children.ForEach(c => c.Visible = !selectedCategory.HasValue || selectedCategory == ((MapEntityPrefab)c.UserData).Category); - return true; + entityList.Content.Children.ForEach(c => + { + c.Visible = !selectedCategory.HasValue || selectedCategory == ((MapEntityPrefab) c.UserData).Category; + if (c.Visible && dummyCharacter?.SelectedConstruction?.OwnInventory != null) + { + c.Visible = c.UserData is MapEntityPrefab item && IsItemPrefab(item); + } + }); + entityList.UpdateScrollBarSize(); + entityList.BarScroll = 0.0f; + + return; } filter = filter.ToLower(); foreach (GUIComponent child in entityList.Content.Children) { var textBlock = child.GetChild(); - child.Visible = - (!selectedCategory.HasValue || selectedCategory == ((MapEntityPrefab)child.UserData).Category) && - ((MapEntityPrefab)child.UserData).Name.ToLower().Contains(filter); + child.Visible = + (!selectedCategory.HasValue || selectedCategory == ((MapEntityPrefab) child.UserData).Category) && + ((MapEntityPrefab) child.UserData).Name.ToLower().Contains(filter); + + if (child.Visible && dummyCharacter?.SelectedConstruction?.OwnInventory != null) + { + child.Visible = child.UserData is MapEntityPrefab item && IsItemPrefab(item); + } } entityList.UpdateScrollBarSize(); entityList.BarScroll = 0.0f; - - return true; } - public bool ClearFilter() + private void ClearFilter() { FilterEntities(""); entityList.UpdateScrollBarSize(); entityList.BarScroll = 0.0f; entityFilterBox.Text = ""; - return true; } public void SetMode(Mode newMode) @@ -1876,30 +2144,8 @@ namespace Barotrauma lockMode = true; defaultModeTickBox.Selected = newMode == Mode.Default; - defaultModeTickBox.CanBeFocused = !defaultModeTickBox.Selected; - - characterModeTickBox.Selected = newMode == Mode.Character; wiringModeTickBox.Selected = newMode == Mode.Wiring; lockMode = false; - - switch (newMode) - { - case Mode.Character: - CreateDummyCharacter(); - break; - case Mode.Wiring: - CreateDummyCharacter(); - var item = new Item(MapEntityPrefab.Find(null, "screwdriver") as ItemPrefab, Vector2.Zero, null); - dummyCharacter.Inventory.TryPutItem(item, null, new List() { InvSlotType.RightHand }); - wiringToolPanel = CreateWiringPanel(); - break; - default: - if (dummyCharacter != null) - { - RemoveDummyCharacter(); - } - break; - } foreach (MapEntity me in MapEntity.mapEntityList) { @@ -1908,17 +2154,23 @@ namespace Barotrauma MapEntity.DeselectAll(); MapEntity.FilteredSelectedList.Clear(); + + CreateDummyCharacter(); + if (newMode == Mode.Wiring) + { + var item = new Item(MapEntityPrefab.Find(null, "screwdriver") as ItemPrefab, Vector2.Zero, null); + dummyCharacter.Inventory.TryPutItem(item, null, new List() { InvSlotType.RightHand }); + wiringToolPanel = CreateWiringPanel(); + } } private void RemoveDummyCharacter() { - if (dummyCharacter == null) { return; } + if (dummyCharacter == null || dummyCharacter.Removed) { return; } foreach (Item item in dummyCharacter.Inventory.Items) { - if (item == null) { continue; } - - item.Remove(); + item?.Remove(); } dummyCharacter.Remove(); @@ -1930,53 +2182,102 @@ namespace Barotrauma List targets = MapEntity.mapEntityList.Any(me => me.IsHighlighted && !MapEntity.SelectedList.Contains(me)) ? MapEntity.mapEntityList.Where(me => me.IsHighlighted).ToList() : new List(MapEntity.SelectedList); - + contextMenu = new GUIListBox(new RectTransform(new Vector2(0.1f, 0.1f), GUI.Canvas) { - MinSize = new Point(180,0), ScreenSpaceOffset = PlayerInput.MousePosition.ToPoint() - }, style: "GUIToolTip"); - - new GUITextBlock(new RectTransform(new Point(contextMenu.Rect.Width, (int)(18 * GUI.Scale)), contextMenu.Content.RectTransform), - TextManager.Get("editor.cut"), font: GUI.SmallFont) + }, style: "GUIToolTip") { - UserData = "cut", - Enabled = targets.Count > 0 - }; - new GUITextBlock(new RectTransform(new Point(contextMenu.Rect.Width, (int)(18 * GUI.Scale)), contextMenu.Content.RectTransform), - TextManager.Get("editor.copytoclipboard"), font: GUI.SmallFont) - { - UserData = "copy", - Enabled = targets.Count > 0 - }; - new GUITextBlock(new RectTransform(new Point(contextMenu.Rect.Width, (int)(18 * GUI.Scale)), contextMenu.Content.RectTransform), - TextManager.Get("editor.paste"), font: GUI.SmallFont) - { - UserData = "paste", - Enabled = MapEntity.CopiedList.Any() - }; - new GUITextBlock(new RectTransform(new Point(contextMenu.Rect.Width, (int)(18 * GUI.Scale)), contextMenu.Content.RectTransform), - TextManager.Get("delete"), font: GUI.SmallFont) - { - UserData = "delete", - Enabled = targets.Count > 0 + Padding = new Vector4(5) }; - foreach (GUITextBlock child in contextMenu.Content.Children) + Item target = null; + + var single = targets.Count == 1 ? targets.Single() : null; + if (single is Item item && item.Components.Any(ic => !(ic is ConnectionPanel) && !(ic is Repairable) && ic.GuiFrame != null)) { - if (!child.Enabled) { child.TextColor *= 0.5f; } + // Do not offer the ability to open the inventory if the inventory should never be drawn + var container = item.GetComponent(); + if (container == null || container.DrawInventory) { target = item; } + } + + // Holding shift brings up special context menu options + if (PlayerInput.IsShiftDown()) + { + new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), + TextManager.Get("CharacterEditor.EditBackgroundColor"), font: GUI.SmallFont) + { + UserData = "bgcolor" + }; + } + else + { + new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), + TextManager.Get("label.openlabel"), font: GUI.SmallFont) + { + UserData = "open", + Enabled = target != null + }; + + new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), + TextManager.Get("editor.cut"), font: GUI.SmallFont) + { + UserData = "cut", + Enabled = targets.Count > 0 + }; + + new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), + TextManager.Get("editor.copytoclipboard"), font: GUI.SmallFont) + { + UserData = "copy", + Enabled = targets.Count > 0 + }; + + new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), + TextManager.Get("editor.paste"), font: GUI.SmallFont) + { + UserData = "paste", + Enabled = MapEntity.CopiedList.Any(), + }; + + new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), + TextManager.Get("delete"), font: GUI.SmallFont) + { + UserData = "delete", + Enabled = targets.Count > 0 + }; } - contextMenu.Content.Children.ForEach(c => c.RectTransform.MinSize = new Point(0, c.Rect.Height)); - contextMenu.RectTransform.NonScaledSize = new Point( - contextMenu.Rect.Width, - (int)((contextMenu.Content.CountChildren * 20) * GUI.Scale)); + foreach (var guiComponent in contextMenu.Content.Children) + { + if (guiComponent is GUITextBlock child) + { + if (!child.Enabled) + { + child.TextColor *= 0.5f; + } + } + } + + contextMenu.Content.Children.ForEach(c => + { + if (c is GUITextBlock block) + { + block.RectTransform.NonScaledSize = new Point((int) (block.TextSize.X + block.Padding.X * 2), (int)(18 * GUI.Scale)); + } + }); + int biggestSize = contextMenu.Content.Children.Max(c => c.Rect.Width + (int)contextMenu.Padding.X * 2); + contextMenu.Content.Children.ForEach(c => c.RectTransform.MinSize = new Point(biggestSize, c.Rect.Height)); + contextMenu.RectTransform.NonScaledSize = new Point(biggestSize, (int)(contextMenu.Content.Children.Sum(c => c.Rect.Height) + (contextMenu.Padding.X * 2))); - contextMenu.OnSelected = (GUIComponent component, object obj) => + contextMenu.OnSelected = (component, obj) => { if (!component.Enabled) { return false; } switch (obj as string) { + case "bgcolor": + CreateBackgroundColorPicker(); + break; case "copy": MapEntity.Copy(targets); break; @@ -1987,7 +2288,10 @@ namespace Barotrauma MapEntity.Paste(cam.ScreenToWorld(contextMenu.Rect.Location.ToVector2())); break; case "delete": - targets.ForEach(me => me.Remove()); + targets.ForEach(me => { me.Remove(); }); + break; + case "open" when target != null: + OpenItem(target); break; } contextMenu = null; @@ -1995,6 +2299,57 @@ namespace Barotrauma }; } + /// + /// Creates a color picker that can be used to change the submarine editor's background color + /// + private void CreateBackgroundColorPicker() + { + var msgBox = new GUIMessageBox(TextManager.Get("CharacterEditor.EditBackgroundColor"), "", new[] { TextManager.Get("Reset"), TextManager.Get("OK")}, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175)); + + var rgbLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.25f), msgBox.Content.RectTransform), isHorizontal: true); + + // Generate R,G,B labels and parent elements + var layoutParents = new GUILayoutGroup[3]; + for (int i = 0; i < 3; i++) + { + var colorContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.33f, 1), rgbLayout.RectTransform), isHorizontal: true) { Stretch = true }; + new GUITextBlock(new RectTransform(new Vector2(0.2f, 1), colorContainer.RectTransform, Anchor.CenterLeft) { MinSize = new Point(15, 0) }, GUI.colorComponentLabels[i], font: GUI.SmallFont, textAlignment: Alignment.Center); + layoutParents[i] = colorContainer; + } + + // attach number inputs to our generated parent elements + var rInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1f), layoutParents[0].RectTransform), GUINumberInput.NumberType.Int) { IntValue = backgroundColor.R }; + var gInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1f), layoutParents[1].RectTransform), GUINumberInput.NumberType.Int) { IntValue = backgroundColor.G }; + var bInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1f), layoutParents[2].RectTransform), GUINumberInput.NumberType.Int) { IntValue = backgroundColor.B }; + + rInput.MinValueInt = gInput.MinValueInt = bInput.MinValueInt = 0; + rInput.MaxValueInt = gInput.MaxValueInt = bInput.MaxValueInt = 255; + + rInput.OnValueChanged = gInput.OnValueChanged = bInput.OnValueChanged = delegate + { + var color = new Color(rInput.IntValue, gInput.IntValue, bInput.IntValue); + backgroundColor = color; + GameSettings.SubEditorBackgroundColor = color; + }; + + // Reset button + msgBox.Buttons[0].OnClicked = (button, o) => + { + rInput.IntValue = 13; + gInput.IntValue = 37; + bInput.IntValue = 69; + return true; + }; + + // Ok button + msgBox.Buttons[1].OnClicked = (button, o) => + { + msgBox.Close(); + GameMain.Config.SaveNewPlayerConfig(); + return true; + }; + } + private GUIFrame CreateWiringPanel() { GUIFrame frame = new GUIFrame(new RectTransform(new Vector2(0.03f, 0.35f), GUI.Canvas) @@ -2027,7 +2382,7 @@ namespace Barotrauma private bool SelectLinkedSub(GUIComponent selected, object userData) { - if (!(selected.UserData is Submarine submarine)) return false; + if (!(selected.UserData is SubmarineInfo submarine)) return false; var prefab = new LinkedSubmarinePrefab(submarine); MapEntityPrefab.SelectPrefab(prefab); return true; @@ -2064,6 +2419,73 @@ namespace Barotrauma } + /// + /// Tries to open an item container in the submarine editor using the dummy character + /// + /// The item we want to open + private void OpenItem(Item itemContainer) + { + if (dummyCharacter == null || itemContainer == null) { return; } + + if ((itemContainer.GetComponent() != null || itemContainer.GetComponent() != null) && itemContainer.GetComponent() != null) + { + // We teleport our dummy character to the item so it appears as the entity stays still when in reality the dummy is holding it + oldItemPosition = itemContainer.SimPosition; + TeleportDummyCharacter(oldItemPosition); + + // Override this so we can be sure the container opens + var container = itemContainer.GetComponent(); + if (container != null) { container.KeepOpenWhenEquipped = true; } + + // We accept any slots except "Any" since that would take priority + List allowedSlots = new List(); + itemContainer.AllowedSlots.ForEach(type => + { + if (type != InvSlotType.Any) { allowedSlots.Add(type); } + }); + + // Try to place the item in the dummy character's inventory + bool success = dummyCharacter.Inventory.TryPutItem(itemContainer, dummyCharacter, allowedSlots); + if (success) { OpenedItem = itemContainer; } + else { return; } + } + MapEntity.SelectedList.Clear(); + MapEntity.FilteredSelectedList.Clear(); + MapEntity.SelectEntity(itemContainer); + dummyCharacter.SelectedConstruction = itemContainer; + FilterEntities(entityFilterBox.Text); + } + + /// + /// Close the currently opened item + /// + private void CloseItem() + { + if (dummyCharacter == null) { return; } + DraggedItemPrefab = null; + dummyCharacter.SelectedConstruction = null; + OpenedItem?.Drop(dummyCharacter); + OpenedItem?.SetTransform(oldItemPosition, 0f); + OpenedItem = null; + FilterEntities(entityFilterBox.Text); + } + + /// + /// Teleports the dummy character to the specified position + /// + /// The desired position + private void TeleportDummyCharacter(Vector2 pos) + { + if (dummyCharacter != null) + { + foreach (Limb limb in dummyCharacter.AnimController.Limbs) + { + limb.body.SetTransform(pos, 0.0f); + } + dummyCharacter.AnimController.Collider.SetTransform(pos, 0); + } + } + private bool ChangeSubName(GUITextBox textBox, string text) { if (string.IsNullOrWhiteSpace(text)) @@ -2082,7 +2504,7 @@ namespace Barotrauma return true; } - private bool ChangeSubDescription(GUITextBox textBox, string text) + private void ChangeSubDescription(GUITextBox textBox, string text) { if (Submarine.MainSub != null) { @@ -2094,8 +2516,26 @@ namespace Barotrauma } submarineDescriptionCharacterCount.Text = text.Length + " / " + submarineDescriptionLimit; + } - return true; + /// + /// Checks if the prefab is an item or if it only consists of items + /// + /// The prefab to check + /// True if the the prefab is an item or it contains only items + private bool IsItemPrefab(MapEntityPrefab mapPrefab) + { + if (dummyCharacter?.SelectedConstruction == null) + { + return false; + } + + return mapPrefab switch + { + ItemPrefab iPrefab => true, + ItemAssemblyPrefab aPrefab => aPrefab.DisplayEntities.All(pair => pair.First is ItemPrefab), + _ => false + }; } private bool SelectPrefab(GUIComponent component, object obj) @@ -2103,7 +2543,7 @@ namespace Barotrauma if (GUI.MouseOn is GUIButton || GUI.MouseOn?.Parent is GUIButton) { return false; } AddPreviouslyUsed(obj as MapEntityPrefab); - + //if selecting a gap/hull/waypoint/spawnpoint, make sure the visibility is toggled on if (obj is CoreEntityPrefab prefab) { @@ -2118,8 +2558,68 @@ namespace Barotrauma } } - MapEntityPrefab.SelectPrefab(obj); - GUI.ForceMouseOn(null); + if (dummyCharacter?.SelectedConstruction != null) + { + var inv = dummyCharacter?.SelectedConstruction?.OwnInventory; + if (inv != null) + { + switch (obj) + { + case ItemAssemblyPrefab assemblyPrefab when PlayerInput.IsShiftDown(): + { + var itemInstance = LoadItemAssemblyInventorySafe(assemblyPrefab); + var spawnedItem = false; + + itemInstance.ForEach(newItem => + { + if (newItem != null) + { + var placedItem = inv.TryPutItem(newItem, dummyCharacter); + spawnedItem |= placedItem; + + if (!placedItem) + { + // Remove everything inside of the item so we don't get the popup asking if we want to keep the contained items + newItem.OwnInventory?.DeleteAllItems(); + newItem.Remove(); + } + } + }); + GUI.PlayUISound(spawnedItem ? GUISoundType.PickItem : GUISoundType.PickItemFail); + break; + } + case ItemPrefab itemPrefab when PlayerInput.IsShiftDown(): + { + var item = new Item(itemPrefab, Vector2.Zero, Submarine.MainSub); + if (!inv.TryPutItem(item, dummyCharacter)) + { + // We failed, remove the item so it doesn't stay at x:0,y:0 + GUI.PlayUISound(GUISoundType.PickItemFail); + item.Remove(); + } + else + { + GUI.PlayUISound(GUISoundType.PickItem); + } + break; + } + case ItemAssemblyPrefab _: + case ItemPrefab _: + { + // Place the item into our hands + DraggedItemPrefab = (MapEntityPrefab) obj; + GUI.PlayUISound(GUISoundType.PickItem); + break; + } + } + } + } + else + { + GUI.PlayUISound(GUISoundType.PickItem); + MapEntityPrefab.SelectPrefab(obj); + } + return false; } @@ -2164,21 +2664,19 @@ namespace Barotrauma } List wallPoints = new List(); - Vector2 min = Vector2.Zero; - Vector2 max = Vector2.Zero; + Vector2 max; List mapEntityList = new List(); foreach (MapEntity e in MapEntity.mapEntityList) { - if (e is Item) + if (e is Item it) { - Item it = e as Item; Door door = it.GetComponent(); if (door != null) { - int halfW = e.WorldRect.Width / 2; - wallPoints.Add(new Vector2(e.WorldRect.X + halfW, -e.WorldRect.Y + e.WorldRect.Height)); + int halfW = it.WorldRect.Width / 2; + wallPoints.Add(new Vector2(it.WorldRect.X + halfW, -it.WorldRect.Y + it.WorldRect.Height)); mapEntityList.Add(it); } continue; @@ -2209,7 +2707,7 @@ namespace Barotrauma return; } - min = wallPoints[0]; + var min = wallPoints[0]; max = wallPoints[0]; for (int i = 0; i < wallPoints.Count; i++) { @@ -2452,8 +2950,7 @@ namespace Barotrauma Rectangle gapRect = e.WorldRect; gapRect.Y -= 8; gapRect.Height = 16; - Gap newGap = new Gap(MapEntityPrefab.Find(null, "gap"), - gapRect); + Gap newGap = new Gap(MapEntityPrefab.Find(null, "gap"), gapRect); } } @@ -2480,29 +2977,42 @@ namespace Barotrauma MapEntity.HighlightedListBox.AddToGUIUpdateList(); } - if ((CharacterMode || WiringMode) && dummyCharacter != null) + if (dummyCharacter != null) { CharacterHUD.AddToGUIUpdateList(dummyCharacter); if (dummyCharacter.SelectedConstruction != null) { dummyCharacter.SelectedConstruction.AddToGUIUpdateList(); } - else if (WiringMode && MapEntity.SelectedList.Count == 1 && MapEntity.SelectedList[0] is Item item && item.GetComponent() != null) + else if (WiringMode && MapEntity.SelectedList.FirstOrDefault() is Item item && item.GetComponent() != null) { - MapEntity.SelectedList[0].AddToGUIUpdateList(); + MapEntity.SelectedList.FirstOrDefault()?.AddToGUIUpdateList(); } } - else + if (loadFrame != null) { - if (loadFrame != null) - { - loadFrame.AddToGUIUpdateList(); - } - else if (saveFrame != null) - { - saveFrame.AddToGUIUpdateList(); - } + loadFrame.AddToGUIUpdateList(); } + else if (saveFrame != null) + { + saveFrame.AddToGUIUpdateList(); + } + } + + /// + /// GUI.MouseOn doesn't get updated while holding primary mouse and we need it to + /// + private bool IsMouseOnEditorGUI() + { + if (GUI.MouseOn == null) + { + return false; + } + + return (EntityMenu?.MouseRect.Contains(PlayerInput.MousePosition) ?? false) + || (entityCountPanel?.MouseRect.Contains(PlayerInput.MousePosition) ?? false) + || (MapEntity.EditingHUD?.MouseRect.Contains(PlayerInput.MousePosition) ?? false) + || (TopPanel?.MouseRect.Contains(PlayerInput.MousePosition) ?? false); } /// @@ -2520,61 +3030,193 @@ namespace Barotrauma UpdateEntityList(); } + if (WiringMode && dummyCharacter != null) + { + Wire equippedWire = + Character.Controlled?.SelectedItems[0]?.GetComponent() ?? + Character.Controlled?.SelectedItems[1]?.GetComponent() ?? + Wire.DraggingWire; + + if (equippedWire == null) + { + // Highlight wires when hovering over the entity selection box + if (MapEntity.HighlightedListBox != null) + { + var lBox = MapEntity.HighlightedListBox; + foreach (var child in lBox.Content.Children) + { + if (child.UserData is Item item) + { + item.ExternalHighlight = GUI.IsMouseOn(child); + } + } + } + + var highlightedEntities = new List(); + + // ReSharper disable once LoopCanBeConvertedToQuery + foreach (Item item in MapEntity.mapEntityList.Where(entity => entity is Item).Cast()) + { + var wire = item.GetComponent(); + if (wire == null || !wire.IsMouseOn()) { continue; } + highlightedEntities.Add(item); + } + + MapEntity.UpdateHighlighting(highlightedEntities, true); + } + } + hullVolumeFrame.Visible = MapEntity.SelectedList.Any(s => s is Hull); + hullVolumeFrame.RectTransform.AbsoluteOffset = new Point(Math.Max(showEntitiesPanel.Rect.Right, previouslyUsedPanel.Rect.Right), 0); saveAssemblyFrame.Visible = MapEntity.SelectedList.Count > 0; + var offset = cam.WorldView.Top - cam.ScreenToWorld(new Vector2(0, GameMain.GraphicsHeight - EntityMenu.Rect.Top)).Y; + + // Move the camera towards to the focus point + if (camTargetFocus != Vector2.Zero) + { + if (GameMain.Config.KeyBind(InputType.Up).IsDown() || GameMain.Config.KeyBind(InputType.Down).IsDown() || + GameMain.Config.KeyBind(InputType.Left).IsDown() || GameMain.Config.KeyBind(InputType.Right).IsDown()) + { + camTargetFocus = Vector2.Zero; + } + else + { + var targetWithOffset = new Vector2(camTargetFocus.X, camTargetFocus.Y - offset / 2); + if (Math.Abs(cam.Position.X - targetWithOffset.X) < 1.0f && + Math.Abs(cam.Position.Y - targetWithOffset.Y) < 1.0f) + { + camTargetFocus = Vector2.Zero; + } + else + { + cam.Position += (targetWithOffset - cam.Position) / cam.MoveSmoothness; + } + } + } + + if (GUI.KeyboardDispatcher.Subscriber == null) { - // TODO adjust when the new inventory stuff rolls in - if (PlayerInput.KeyHit(Keys.Q) && mode == Mode.Default) + if (PlayerInput.KeyHit(Keys.E) && mode == Mode.Default) + { + if (dummyCharacter != null) + { + if (dummyCharacter.SelectedConstruction == null) + { + foreach (var entity in MapEntity.mapEntityList) + { + if (entity is Item item && entity.IsHighlighted && item.Components.Any(ic => !(ic is ConnectionPanel) && !(ic is Repairable) && ic.GuiFrame != null)) + { + var container = item.GetComponents().ToList(); + if (!container.Any() || container.Any(ic => ic?.DrawInventory ?? false)) + { + OpenItem(item); + break; + } + } + } + } + else + { + CloseItem(); + } + } + } + + // Focus to selection + if (PlayerInput.KeyHit(Keys.F) && mode == Mode.Default) + { + // content warning: contains coordinate system workarounds + var selected = MapEntity.SelectedList; + if (selected.Count > 0) + { + var dRect = selected.First().Rect; + var rect = new Rectangle(dRect.Left, dRect.Top, dRect.Width, dRect.Height * -1); + if (selected.Count > 1) + { + // Create one big rect out of our selection + selected.Skip(1).ForEach(me => + { + var wRect = me.Rect; + rect = Rectangle.Union(rect, new Rectangle(wRect.Left, wRect.Top, wRect.Width, wRect.Height * -1)); + }); + } + camTargetFocus = rect.Center.ToVector2(); + } + } + + if (GameMain.Config.KeyBind(InputType.ToggleInventory).IsHit() && mode == Mode.Default) { toggleEntityMenuButton.OnClicked?.Invoke(toggleEntityMenuButton, toggleEntityMenuButton.UserData); } - + if (PlayerInput.KeyHit(Keys.Tab)) { entityFilterBox.Select(); } - if (PlayerInput.KeyDown(Keys.LeftControl)) + if (PlayerInput.IsCtrlDown() && MapEntity.StartMovingPos == Vector2.Zero) { + cam.MoveCamera((float) deltaTime, allowMove: false); // Save menu if (PlayerInput.KeyHit(Keys.S)) { - if (PlayerInput.KeyDown(Keys.LeftShift)) + if (PlayerInput.IsShiftDown()) { - // Save the sub without a menu - if (subNameLabel != null) - { - SaveSubToFile(subNameLabel.Text); - } + // Quick-save, but only when we've set a custom name for our sub + CreateSaveScreen(subNameLabel != null && subNameLabel.Text != TextManager.Get("unspecifiedsubfilename")); } else { // Save menu - if (saveFrame == null) + CreateSaveScreen(); + } + } + + // Select or deselect everything + if (PlayerInput.KeyHit(Keys.A) && mode == Mode.Default) + { + if (MapEntity.SelectedList.Any()) + { + MapEntity.DeselectAll(); + } + else + { + var selectables = MapEntity.mapEntityList.Where(entity => entity.SelectableInEditor).ToList(); + lock (selectables) { - CreateSaveScreen(); + selectables.ForEach(MapEntity.AddSelection); } } } - // 1-3 keys on the keyboard for switching modes + // 1-2 keys on the keyboard for switching modes if (PlayerInput.KeyHit(Keys.D1)) { SetMode(Mode.Default); } - if (PlayerInput.KeyHit(Keys.D2)) { SetMode(Mode.Character); } - if (PlayerInput.KeyHit(Keys.D3)) { SetMode(Mode.Wiring); } + if (PlayerInput.KeyHit(Keys.D2)) { SetMode(Mode.Wiring); } } else { - cam.MoveCamera((float) deltaTime, true); + cam.MoveCamera((float) deltaTime, allowMove: true); } } + else + { + cam.MoveCamera((float) deltaTime, allowMove: false); + } if (PlayerInput.MidButtonHeld()) { - Vector2 moveSpeed = PlayerInput.MouseSpeed * (float)deltaTime * 100.0f / cam.Zoom; + Vector2 moveSpeed = PlayerInput.MouseSpeed * (float)deltaTime * 60.0f / cam.Zoom; moveSpeed.X = -moveSpeed.X; cam.Position += moveSpeed; + // break out of trying to focus + camTargetFocus = Vector2.Zero; + } + + if (PlayerInput.KeyHit(Keys.Escape) && dummyCharacter != null) + { + CloseItem(); } if (contextMenu != null) @@ -2587,20 +3229,16 @@ namespace Barotrauma } } - if (CharacterMode || WiringMode) + if (dummyCharacter != null && Entity.FindEntityByID(dummyCharacter.ID) == dummyCharacter) { - if (dummyCharacter == null || Entity.FindEntityByID(dummyCharacter.ID) != dummyCharacter) - { - SetMode(Mode.Default); - } - else + if (WiringMode) { foreach (MapEntity me in MapEntity.mapEntityList) { me.IsHighlighted = false; } - if (WiringMode && dummyCharacter.SelectedConstruction == null) + if (dummyCharacter.SelectedConstruction == null) { List wires = new List(); foreach (Item item in Item.ItemList) @@ -2610,68 +3248,251 @@ namespace Barotrauma } Wire.UpdateEditing(wires); } + } - if (dummyCharacter.SelectedConstruction == null || - dummyCharacter.SelectedConstruction.GetComponent() != null) + if (!WiringMode) + { + // Move all of our slots on top center of the entity list + // We use the slots to open item inventories and we want the position of them to be consisent + dummyCharacter.Inventory.slots.ForEach(slot => { - if (WiringMode && (PlayerInput.KeyDown(Microsoft.Xna.Framework.Input.Keys.LeftShift) || PlayerInput.KeyDown(Microsoft.Xna.Framework.Input.Keys.Right))) + slot.Rect.Y = EntityMenu.Rect.Top; + slot.Rect.X = EntityMenu.Rect.X + (EntityMenu.Rect.Width / 2) - (slot.Rect.Width /2); + }); + } + + if (dummyCharacter.SelectedConstruction == null || + dummyCharacter.SelectedConstruction.GetComponent() != null) + { + if (WiringMode && PlayerInput.IsShiftDown()) + { + Wire equippedWire = + Character.Controlled?.SelectedItems[0]?.GetComponent() ?? + Character.Controlled?.SelectedItems[1]?.GetComponent(); + if (equippedWire != null && equippedWire.GetNodes().Count > 0) { - Wire equippedWire = - Character.Controlled?.SelectedItems[0]?.GetComponent() ?? - Character.Controlled?.SelectedItems[1]?.GetComponent(); - if (equippedWire != null && equippedWire.GetNodes().Count > 0) + Vector2 lastNode = equippedWire.GetNodes().Last(); + if (equippedWire.Item.Submarine != null) { - Vector2 lastNode = equippedWire.GetNodes().Last(); - if (equippedWire.Item.Submarine != null) - { - lastNode += equippedWire.Item.Submarine.HiddenSubPosition + equippedWire.Item.Submarine.Position; - } - - dummyCharacter.CursorPosition = - Math.Abs(dummyCharacter.CursorPosition.X - lastNode.X) < Math.Abs(dummyCharacter.CursorPosition.Y - lastNode.Y) ? - new Vector2(lastNode.X, dummyCharacter.CursorPosition.Y) : - dummyCharacter.CursorPosition = new Vector2(dummyCharacter.CursorPosition.X, lastNode.Y); + lastNode += equippedWire.Item.Submarine.HiddenSubPosition + equippedWire.Item.Submarine.Position; } - } - Vector2 mouseSimPos = FarseerPhysics.ConvertUnits.ToSimUnits(dummyCharacter.CursorPosition); - foreach (Limb limb in dummyCharacter.AnimController.Limbs) - { - limb.body.SetTransform(mouseSimPos, 0.0f); + var (cursorX, cursorY) = dummyCharacter.CursorPosition; + + bool isHorizontal = Math.Abs(cursorX - lastNode.X) < Math.Abs(cursorY - lastNode.Y); + + float roundedY = MathUtils.Round(cursorY, Submarine.GridSize.Y / 2.0f); + float roundedX = MathUtils.Round(cursorX, Submarine.GridSize.X / 2.0f); + + dummyCharacter.CursorPosition = isHorizontal + ? new Vector2(lastNode.X, roundedY) + : new Vector2(roundedX, lastNode.Y); } - dummyCharacter.AnimController.Collider.SetTransform(mouseSimPos, 0.0f); } + // Keep teleporting the dummy character to the opened item to make it look like the container didn't go anywhere + if (OpenedItem != null) + { + TeleportDummyCharacter(oldItemPosition); + } + + if (WiringMode && dummyCharacter?.SelectedConstruction == null) + { + TeleportDummyCharacter(FarseerPhysics.ConvertUnits.ToSimUnits(dummyCharacter.CursorPosition)); + } + } + + if (WiringMode) + { dummyCharacter.ControlLocalPlayer((float)deltaTime, cam, false); dummyCharacter.Control((float)deltaTime, cam); + } - dummyCharacter.Submarine = Submarine.MainSub; + cam.TargetPos = Vector2.Zero; + dummyCharacter.Submarine = Submarine.MainSub; + } - cam.TargetPos = Vector2.Zero; + // Deposit item from our "infinite stack" into inventory slots + var inv = dummyCharacter?.SelectedConstruction?.OwnInventory; + if (inv?.slots != null) + { + var dragginMouse = MouseDragStart != Vector2.Zero && Vector2.Distance(PlayerInput.MousePosition, MouseDragStart) >= GUI.Scale * 20; + + // So we don't accidentally drag inventory items while doing this + if (DraggedItemPrefab != null) { Inventory.draggingItem = null; } + + switch (DraggedItemPrefab) + { + // regular item prefabs + case ItemPrefab itemPrefab when PlayerInput.PrimaryMouseButtonClicked() || dragginMouse: + { + bool spawnedItem = false; + for (var i = 0; i < inv.slots.Length; i++) + { + var slot = inv.slots[i]; + var itemContainer = inv?.Items[i]?.GetComponent(); + + // check if the slot is empty or if we can place the item into a container, for example an oxygen tank into a diving suit + if (Inventory.IsMouseOnSlot(slot)) + { + var newItem = new Item(itemPrefab, Vector2.Zero, Submarine.MainSub); + + if (inv.Items[i] == null) + { + bool placedItem = inv.TryPutItem(newItem, i, false, true, dummyCharacter); + spawnedItem |= placedItem; + + if (!placedItem) + { + newItem.Remove(); + } + } + else if (itemContainer != null && itemContainer.CanBeContained(itemPrefab) && + (itemContainer.Inventory?.Items.Any(item => item == null) ?? false)) + { + bool placedItem = itemContainer.Inventory.TryPutItem(newItem, dummyCharacter); + spawnedItem |= placedItem; + + // try to place the item into the inventory of the item we are hovering over + if (!placedItem) + { + newItem.Remove(); + } + else + { + slot.ShowBorderHighlight(GUI.Style.Green, 0.1f, 0.4f); + } + } + else + { + newItem.Remove(); + } + + if (!dragginMouse) + { + GUI.PlayUISound(spawnedItem ? GUISoundType.PickItem : GUISoundType.PickItemFail); + } + } + } + break; + } + // item assemblies + case ItemAssemblyPrefab assemblyPrefab when PlayerInput.PrimaryMouseButtonClicked(): + { + bool spawnedItems = false; + for (var i = 0; i < inv.slots.Length; i++) + { + var slot = inv.slots[i]; + var itemContainer = inv?.Items[i]?.GetComponent(); + if (inv.Items[i] == null && Inventory.IsMouseOnSlot(slot)) + { + // load the items + var itemInstance = LoadItemAssemblyInventorySafe(assemblyPrefab); + + // counter for items that failed so we so we known that slot remained empty + var failedCount = 0; + + for (var j = 0; j < itemInstance.Count(); j++) + { + var newItem = itemInstance[j]; + var newSpot = i + j - failedCount; + + // try to find a valid slot to put the items + while (inv.slots.Length > newSpot) + { + if (inv.Items[newSpot] == null) { break; } + newSpot++; + } + + // valid slot found + if (inv.slots.Length > newSpot) + { + var placedItem = inv.TryPutItem(newItem, newSpot, false, true, dummyCharacter); + spawnedItems |= placedItem; + + if (!placedItem) + { + failedCount++; + // delete the included items too so we don't get a popup asking if we want to keep them + newItem?.OwnInventory?.DeleteAllItems(); + newItem.Remove(); + } + } + else + { + var placedItem = inv.TryPutItem(newItem, dummyCharacter); + spawnedItems |= placedItem; + + // if our while loop didn't find a valid slot then let the inventory decide where to put it as a last resort + if (!placedItem) + { + // delete the included items too so we don't get a popup asking if we want to keep them + newItem?.OwnInventory?.DeleteAllItems(); + newItem.Remove(); + } + } + } + } + } + GUI.PlayUISound(spawnedItems ? GUISoundType.PickItem : GUISoundType.PickItemFail); + break; + } } } - else if (!saveAssemblyFrame.Rect.Contains(PlayerInput.MousePosition)) + + // Update our mouse dragging state so we can easily slide thru slots while holding the mouse button down to place lots of items + if (PlayerInput.PrimaryMouseButtonHeld()) + { + if (MouseDragStart == Vector2.Zero) + { + MouseDragStart = PlayerInput.MousePosition; + } + } + else + { + MouseDragStart = Vector2.Zero; + } + + if (!saveAssemblyFrame.Rect.Contains(PlayerInput.MousePosition) && dummyCharacter?.SelectedConstruction == null && !WiringMode && GUI.MouseOn == null) { MapEntity.UpdateSelecting(cam); } - - if (!CharacterMode && !WiringMode) + + if (!WiringMode) { + bool shouldCloseHud = dummyCharacter?.SelectedConstruction != null && HUD.CloseHUD(dummyCharacter.SelectedConstruction.Rect) && DraggedItemPrefab == null; + if (MapEntityPrefab.Selected != null && GUI.MouseOn == null) { MapEntityPrefab.Selected.UpdatePlacing(cam); } else { - if (PlayerInput.RightButtonClicked()) + if (PlayerInput.SecondaryMouseButtonClicked() && !shouldCloseHud) { - CreateContextMenu(); + if (GUI.IsMouseOn(entityFilterBox)) + { + ClearFilter(); + } + else + { + if (dummyCharacter?.SelectedConstruction == null) + { + CreateContextMenu(); + } + DraggedItemPrefab = null; + } + } + + if (shouldCloseHud) + { + CloseItem(); } } MapEntity.UpdateEditor(cam); } - entityMenuOpenState = entityMenuOpen && !CharacterMode & !WiringMode ? + entityMenuOpenState = entityMenuOpen && !WiringMode ? (float)Math.Min(entityMenuOpenState + deltaTime * 5.0f, 1.0f) : (float)Math.Max(entityMenuOpenState - deltaTime * 5.0f, 0.0f); @@ -2705,7 +3526,7 @@ namespace Barotrauma } } - if ((CharacterMode || WiringMode) && dummyCharacter != null) + if (dummyCharacter != null) { dummyCharacter.AnimController.FindHull(dummyCharacter.CursorWorldPosition, false); @@ -2719,34 +3540,29 @@ namespace Barotrauma //wires need to be updated for the last node to follow the player during rewiring Wire wire = item.GetComponent(); - if (wire != null) wire.Update((float)deltaTime, cam); + wire?.Update((float)deltaTime, cam); } if (dummyCharacter.SelectedConstruction != null) { - if (dummyCharacter.SelectedConstruction != null) + if (MapEntity.SelectedList.Contains(dummyCharacter.SelectedConstruction) || WiringMode) { - dummyCharacter.SelectedConstruction.UpdateHUD(cam, dummyCharacter, (float)deltaTime); + dummyCharacter.SelectedConstruction?.UpdateHUD(cam, dummyCharacter, (float)deltaTime); } - - //if (PlayerInput.KeyHit(InputType.Select) && dummyCharacter.FocusedItem != dummyCharacter.SelectedConstruction && GUI.KeyboardDispatcher.Subscriber == null) - //{ - // dummyCharacter.SelectedConstruction = null; - //} - /*if (PlayerInput.KeyHit(InputType.Deselect)) + else { - dummyCharacter.SelectedConstruction = null; - }*/ + // We somehow managed to unfocus the item, close it so our framerate doesn't go to 5 because the + // UpdateHUD() method keeps re-creating the editing HUD + CloseItem(); + } } - else if (MapEntity.SelectedList.Count == 1) + else if (MapEntity.SelectedList.Count == 1 && WiringMode) { (MapEntity.SelectedList[0] as Item)?.UpdateHUD(cam, dummyCharacter, (float)deltaTime); } CharacterHUD.Update((float)deltaTime, dummyCharacter, cam); } - - //GUI.Update((float)deltaTime); } /// @@ -2766,26 +3582,44 @@ namespace Barotrauma } spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, transformMatrix: cam.Transform); - graphics.Clear(new Color(0.051f, 0.149f, 0.271f, 1.0f)); + graphics.Clear(backgroundColor); if (GameMain.DebugDraw) { GUI.DrawLine(spriteBatch, new Vector2(Submarine.MainSub.HiddenSubPosition.X, -cam.WorldView.Y), new Vector2(Submarine.MainSub.HiddenSubPosition.X, -(cam.WorldView.Y - cam.WorldView.Height)), Color.White * 0.5f, 1.0f, (int)(2.0f / cam.Zoom)); GUI.DrawLine(spriteBatch, new Vector2(cam.WorldView.X, -Submarine.MainSub.HiddenSubPosition.Y), new Vector2(cam.WorldView.Right, -Submarine.MainSub.HiddenSubPosition.Y), Color.White * 0.5f, 1.0f, (int)(2.0f / cam.Zoom)); } - Submarine.DrawBack(spriteBatch, true, e => e is Structure s && (e.SpriteDepth >= 0.9f || s.Prefab.BackgroundSprite != null)); + Submarine.DrawBack(spriteBatch, true, e => + e is Structure s && + (ShowThalamus || !s.prefab.Category.HasFlag(MapEntityCategory.Thalamus)) && + (e.SpriteDepth >= 0.9f || s.Prefab.BackgroundSprite != null)); spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, transformMatrix: cam.Transform); - Submarine.DrawBack(spriteBatch, true, e => !(e is Structure) || e.SpriteDepth < 0.9f); + + // When we "open" a wearable item with inventory it won't get rendered because the dummy character is invisible + // So we are drawing a clone of it on the same position + if (OpenedItem?.GetComponent() != null) + { + OpenedItem.Sprite.Draw(spriteBatch, new Vector2(OpenedItem.DrawPosition.X, -(OpenedItem.DrawPosition.Y)), + scale: OpenedItem.Scale, color: OpenedItem.SpriteColor, depth: OpenedItem.SpriteDepth); + GUI.DrawRectangle(spriteBatch, + new Vector2(OpenedItem.WorldRect.X, -OpenedItem.WorldRect.Y), + new Vector2(OpenedItem.Rect.Width, OpenedItem.Rect.Height), + Color.White, false, 0, (int)Math.Max(2.0f / cam.Zoom, 1.0f)); + } + + Submarine.DrawBack(spriteBatch, true, e => + (!(e is Structure) || e.SpriteDepth < 0.9f) && + (ShowThalamus || !e.prefab.Category.HasFlag(MapEntityCategory.Thalamus))); spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, transformMatrix: cam.Transform); - Submarine.DrawDamageable(spriteBatch, null, editing: true); + Submarine.DrawDamageable(spriteBatch, null, editing: true, e => ShowThalamus || !(e.prefab?.Category.HasFlag(MapEntityCategory.Thalamus) ?? false)); spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, transformMatrix: cam.Transform); - Submarine.DrawFront(spriteBatch, editing: true); - if (!CharacterMode && !WiringMode && GUI.MouseOn == null) + Submarine.DrawFront(spriteBatch, editing: true, e => ShowThalamus || !(e.prefab?.Category.HasFlag(MapEntityCategory.Thalamus) ?? false)); + if (!WiringMode && !IsMouseOnEditorGUI()) { MapEntityPrefab.Selected?.DrawPlacing(spriteBatch, cam); MapEntity.DrawSelecting(spriteBatch, cam); @@ -2794,7 +3628,7 @@ namespace Barotrauma if (GameMain.LightManager.LightingEnabled && lightingEnabled) { - spriteBatch.Begin(SpriteSortMode.Deferred, Lights.CustomBlendStates.Multiplicative, null, DepthStencilState.None, null, null, null); + spriteBatch.Begin(SpriteSortMode.Deferred, Lights.CustomBlendStates.Multiplicative, null, DepthStencilState.None); spriteBatch.Draw(GameMain.LightManager.LightMap, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), Color.White); spriteBatch.End(); } @@ -2830,24 +3664,22 @@ namespace Barotrauma } } - if ((CharacterMode || WiringMode) && dummyCharacter != null) + if (dummyCharacter != null) { - dummyCharacter.DrawHUD(spriteBatch, cam, false); - if (WiringMode) wiringToolPanel.DrawManually(spriteBatch); - } - else - { - MapEntity.DrawEditor(spriteBatch, cam); + if (WiringMode) + { + dummyCharacter.DrawHUD(spriteBatch, cam, false); + wiringToolPanel.DrawManually(spriteBatch); + } } + MapEntity.DrawEditor(spriteBatch, cam); GUI.Draw(Cam, spriteBatch); - - if (!PlayerInput.PrimaryMouseButtonHeld()) Inventory.draggingItem = null; spriteBatch.End(); } - private void CreateImage(int width, int height, Stream stream) + private void CreateImage(int width, int height, System.IO.Stream stream) { MapEntity.SelectedList.Clear(); @@ -2882,7 +3714,7 @@ namespace Barotrauma }*/ spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, null, null, null, null, transform); - Submarine.Draw(spriteBatch, false); + Submarine.Draw(spriteBatch); Submarine.DrawFront(spriteBatch); Submarine.DrawDamageable(spriteBatch, null); spriteBatch.End(); @@ -2899,11 +3731,11 @@ namespace Barotrauma public void SaveScreenShot(int width, int height, string filePath) { - Stream stream = File.OpenWrite(filePath); + System.IO.Stream stream = File.OpenWrite(filePath); CreateImage(width, height, stream); stream.Dispose(); } - + public static bool IsSubEditor() { return Screen.Selected is SubEditorScreen && !Submarine.Unloading; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index 5cf2e0892..31c7fbd53 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -392,22 +392,40 @@ namespace Barotrauma public GUIComponent CreateBoolField(ISerializableEntity entity, SerializableProperty property, bool value, string displayName, string toolTip) { - GUITickBox propertyTickBox = new GUITickBox(new RectTransform(new Point(Rect.Width, elementHeight), layoutGroup.RectTransform, isFixedSize: true), displayName) + var editableAttribute = property.GetAttribute(); + if (editableAttribute.ReadOnly) { - Font = GUI.SmallFont, - Selected = value, - ToolTip = toolTip, - OnSelected = (tickBox) => + var frame = new GUIFrame(new RectTransform(new Point(Rect.Width, Math.Max(elementHeight, 26)), layoutGroup.RectTransform, isFixedSize: true), color: Color.Transparent); + var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUI.SmallFont) { - if (property.TrySetValue(entity, tickBox.Selected)) + ToolTip = toolTip + }; + var valueField = new GUITextBlock(new RectTransform(new Vector2(inputFieldWidth, 1), frame.RectTransform, Anchor.TopRight), value.ToString()) + { + ToolTip = toolTip, + Font = GUI.SmallFont + }; + return valueField; + } + else + { + GUITickBox propertyTickBox = new GUITickBox(new RectTransform(new Point(Rect.Width, elementHeight), layoutGroup.RectTransform, isFixedSize: true), displayName) + { + Font = GUI.SmallFont, + Selected = value, + ToolTip = toolTip, + OnSelected = (tickBox) => { - TrySendNetworkUpdate(entity, property); + if (SetPropertyValue(property, entity, tickBox.Selected)) + { + TrySendNetworkUpdate(entity, property); + } + return true; } - return true; - } - }; - if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name, new GUIComponent[] { propertyTickBox }); } - return propertyTickBox; + }; + if (!Fields.ContainsKey(property.Name)) { Fields.Add(property.Name, new GUIComponent[] { propertyTickBox }); } + return propertyTickBox; + } } public GUIComponent CreateIntField(ISerializableEntity entity, SerializableProperty property, int value, string displayName, string toolTip) @@ -440,7 +458,7 @@ namespace Barotrauma numberInput.IntValue = value; numberInput.OnValueChanged += (numInput) => { - if (property.TrySetValue(entity, numInput.IntValue)) + if (SetPropertyValue(property, entity, numInput.IntValue)) { TrySendNetworkUpdate(entity, property); } @@ -453,8 +471,11 @@ namespace Barotrauma public GUIComponent CreateFloatField(ISerializableEntity entity, SerializableProperty property, float value, string displayName, string toolTip) { - var frame = new GUIFrame(new RectTransform(new Point(Rect.Width, Math.Max(elementHeight, 26)), layoutGroup.RectTransform, isFixedSize: true), color: Color.Transparent); - var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - 1, 1), frame.RectTransform), displayName, font: GUI.SmallFont) + var frame = new GUIFrame(new RectTransform(new Point(Rect.Width, Math.Max(elementHeight, 26)), layoutGroup.RectTransform, isFixedSize: true), color: Color.Transparent) + { + CanBeFocused = false + }; + var label = new GUITextBlock(new RectTransform(new Vector2(1.0f - inputFieldWidth, 1), frame.RectTransform), displayName, font: GUI.SmallFont) { ToolTip = toolTip }; @@ -474,7 +495,7 @@ namespace Barotrauma numberInput.OnValueChanged += (numInput) => { - if (property.TrySetValue(entity, numInput.FloatValue)) + if (SetPropertyValue(property, entity, numInput.FloatValue)) { // This causes stack overflow. What's the purpose of it? //numInput.FloatValue = (float)property.GetValue(entity); @@ -504,7 +525,7 @@ namespace Barotrauma enumDropDown.SelectItem(value); enumDropDown.OnSelected += (selected, val) => { - if (property.TrySetValue(entity, val)) + if (SetPropertyValue(property, entity, val)) { TrySendNetworkUpdate(entity, property); } @@ -536,7 +557,7 @@ namespace Barotrauma } enumDropDown.OnSelected += (selected, val) => { - if (property.TrySetValue(entity, string.Join(", ", enumDropDown.SelectedDataMultiple.Select(d => d.ToString())))) + if (SetPropertyValue(property, entity, string.Join(", ", enumDropDown.SelectedDataMultiple.Select(d => d.ToString())))) { TrySendNetworkUpdate(entity, property); } @@ -568,16 +589,23 @@ namespace Barotrauma ToolTip = toolTip, Font = GUI.SmallFont, Text = value, - OverflowClip = true, + OverflowClip = true }; - propertyBox.OnDeselected += (textBox, keys) => + + propertyBox.OnDeselected += (textBox, keys) => OnApply(textBox); + propertyBox.OnEnterPressed += (box, text) => OnApply(box); + + bool OnApply(GUITextBox textBox) { - if (property.TrySetValue(entity, textBox.Text)) + if (SetPropertyValue(property, entity, textBox.Text)) { TrySendNetworkUpdate(entity, property); - textBox.Text = (string)property.GetValue(entity); + textBox.Text = (string) property.GetValue(entity); + textBox.Flash(GUI.Style.Green, flashDuration: 1f); } - }; + return true; + } + if (translationTextTag != null) { new GUIButton(new RectTransform(new Vector2(browseButtonWidth, 1), frame.RectTransform, Anchor.TopRight), "...", style: "GUIButtonSmall") @@ -623,7 +651,14 @@ namespace Barotrauma for (int i = 1; i >= 0; i--) { var element = new GUIFrame(new RectTransform(new Vector2(0.45f, 1), inputArea.RectTransform), style: null); - new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), GUI.vectorComponentLabels[i], font: GUI.SmallFont, textAlignment: Alignment.CenterLeft); + + string componentLabel = GUI.vectorComponentLabels[i]; + if (editableAttribute.VectorComponentLabels != null && i < editableAttribute.VectorComponentLabels.Length) + { + componentLabel = TextManager.Get(editableAttribute.VectorComponentLabels[i]); + } + + new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), componentLabel, font: GUI.SmallFont, textAlignment: Alignment.Center); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), GUINumberInput.NumberType.Int) { @@ -647,7 +682,7 @@ namespace Barotrauma else newVal.Y = numInput.IntValue; - if (property.TrySetValue(entity, newVal)) + if (SetPropertyValue(property, entity, newVal)) { TrySendNetworkUpdate(entity, property); } @@ -676,7 +711,13 @@ namespace Barotrauma for (int i = 1; i >= 0; i--) { var element = new GUIFrame(new RectTransform(new Vector2(0.45f, 1), inputArea.RectTransform), style: null); - new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), GUI.vectorComponentLabels[i], font: GUI.SmallFont, textAlignment: Alignment.CenterLeft); + + string componentLabel = GUI.vectorComponentLabels[i]; + if (editableAttribute.VectorComponentLabels != null && i < editableAttribute.VectorComponentLabels.Length) + { + componentLabel = TextManager.Get(editableAttribute.VectorComponentLabels[i]); + } + new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), componentLabel, font: GUI.SmallFont, textAlignment: Alignment.Center); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), GUINumberInput.NumberType.Float) { @@ -702,7 +743,7 @@ namespace Barotrauma else newVal.Y = numInput.FloatValue; - if (property.TrySetValue(entity, newVal)) + if (SetPropertyValue(property, entity, newVal)) { TrySendNetworkUpdate(entity, property); } @@ -731,7 +772,14 @@ namespace Barotrauma for (int i = 2; i >= 0; i--) { var element = new GUIFrame(new RectTransform(new Vector2(0.33f, 1), inputArea.RectTransform), style: null); - new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), GUI.vectorComponentLabels[i], font: GUI.SmallFont, textAlignment: Alignment.CenterLeft); + + string componentLabel = GUI.vectorComponentLabels[i]; + if (editableAttribute.VectorComponentLabels != null && i < editableAttribute.VectorComponentLabels.Length) + { + componentLabel = TextManager.Get(editableAttribute.VectorComponentLabels[i]); + } + + new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), componentLabel, font: GUI.SmallFont, textAlignment: Alignment.Center); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), GUINumberInput.NumberType.Float) { @@ -761,7 +809,7 @@ namespace Barotrauma else newVal.Z = numInput.FloatValue; - if (property.TrySetValue(entity, newVal)) + if (SetPropertyValue(property, entity, newVal)) { TrySendNetworkUpdate(entity, property); } @@ -790,7 +838,14 @@ namespace Barotrauma for (int i = 3; i >= 0; i--) { var element = new GUIFrame(new RectTransform(new Vector2(0.22f, 1), inputArea.RectTransform) { MinSize = new Point(50, 0), MaxSize = new Point(150, 50) }, style: null); - new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), GUI.vectorComponentLabels[i], font: GUI.SmallFont, textAlignment: Alignment.CenterLeft); + + string componentLabel = GUI.vectorComponentLabels[i]; + if (editableAttribute.VectorComponentLabels != null && i < editableAttribute.VectorComponentLabels.Length) + { + componentLabel = TextManager.Get(editableAttribute.VectorComponentLabels[i]); + } + + new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), componentLabel, font: GUI.SmallFont, textAlignment: Alignment.Center); GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), GUINumberInput.NumberType.Float) { @@ -824,7 +879,7 @@ namespace Barotrauma else newVal.W = numInput.FloatValue; - if (property.TrySetValue(entity, newVal)) + if (SetPropertyValue(property, entity, newVal)) { TrySendNetworkUpdate(entity, property); } @@ -894,7 +949,7 @@ namespace Barotrauma else newVal.A = (byte)(numInput.IntValue); - if (property.TrySetValue(entity, newVal)) + if (SetPropertyValue(property, entity, newVal)) { TrySendNetworkUpdate(entity, property); colorBox.Color = newVal; @@ -958,7 +1013,7 @@ namespace Barotrauma else newVal.Height = numInput.IntValue; - if (property.TrySetValue(entity, newVal)) + if (SetPropertyValue(property, entity, newVal)) { TrySendNetworkUpdate(entity, property); } @@ -980,7 +1035,7 @@ namespace Barotrauma { string text = userData as string ?? ""; - if (property.TrySetValue(entity, text)) + if (SetPropertyValue(property, entity, text)) { TrySendNetworkUpdate(entity, property); textBox.Text = (string)property.GetValue(entity); @@ -1019,6 +1074,62 @@ namespace Barotrauma } } } - } + private bool SetPropertyValue(SerializableProperty property, object entity, object value) + { + MultiSetProperties(property, entity, value); + return property.TrySetValue(entity, value); + } + + /// + /// Sets common shared properties to all selected map entities in sub editor. + /// Only works client side while in the sub editor and when parentObject is ItemComponent, Item or Structure. + /// + /// + /// + /// + /// The function has the same parameters as + private void MultiSetProperties(SerializableProperty property, object parentObject, object value) + { + if (!(Screen.Selected is SubEditorScreen) || MapEntity.SelectedList.Count <= 1) { return; } + if (!(parentObject is ItemComponent || parentObject is Item || parentObject is Structure || parentObject is Hull)) { return; } + + foreach (var entity in MapEntity.SelectedList.Where(entity => entity != parentObject)) + { + switch (parentObject) + { + case Hull _: + case Structure _: + case Item _: + { + if (entity.GetType() == parentObject.GetType()) + { + property.PropertyInfo.SetValue(entity, value); + } + else if (entity is ISerializableEntity sEntity && sEntity.SerializableProperties != null) + { + var props = sEntity.SerializableProperties; + + if (props.TryGetValue(property.NameToLowerInvariant, out SerializableProperty foundProp)) + { + foundProp.PropertyInfo.SetValue(entity, value); + } + } + break; + } + case ItemComponent _ when entity is Item item: + { + foreach (var component in item.Components) + { + if (component.GetType() == parentObject.GetType() && component != parentObject) + { + property.PropertyInfo.SetValue(component, value); + } + } + break; + } + } + } + } + } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs index b6d19a664..700007424 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/Sound.cs @@ -1,7 +1,7 @@ using System; using OpenAL; using Microsoft.Xna.Framework; -using System.IO; +using Barotrauma.IO; using System.Xml.Linq; namespace Barotrauma.Sounds diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs index f1b73d220..9c64fa486 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs @@ -205,7 +205,7 @@ namespace Barotrauma.Sounds get { return gain; } set { - gain = Math.Max(Math.Min(value, 1.0f), 0.0f); + gain = Math.Clamp(value, 0.0f, 1.0f); if (ALSourceIndex < 0) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs index bca402c77..3c669361d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs @@ -5,7 +5,7 @@ using System.Xml.Linq; using OpenAL; using Microsoft.Xna.Framework; using System.Linq; -using System.IO; +using Barotrauma.IO; namespace Barotrauma.Sounds { @@ -286,7 +286,7 @@ namespace Barotrauma.Sounds if (!File.Exists(filename)) { - throw new FileNotFoundException("Sound file \"" + filename + "\" doesn't exist!"); + throw new System.IO.FileNotFoundException("Sound file \"" + filename + "\" doesn't exist!"); } Sound newSound = new OggSound(this, filename, stream, null); @@ -297,14 +297,14 @@ namespace Barotrauma.Sounds return newSound; } - public Sound LoadSound(XElement element, bool stream = false) + public Sound LoadSound(XElement element, bool stream = false, string overrideFilePath = null) { if (Disabled) { return null; } - string filePath = element.GetAttributeString("file", ""); + string filePath = overrideFilePath ?? element.GetAttributeString("file", ""); if (!File.Exists(filePath)) { - throw new FileNotFoundException("Sound file \"" + filePath + "\" doesn't exist!"); + throw new System.IO.FileNotFoundException("Sound file \"" + filePath + "\" doesn't exist!"); } var newSound = new OggSound(this, filePath, stream, xElement: element); @@ -621,7 +621,10 @@ namespace Barotrauma.Sounds } if (streamingThread == null || streamingThread.ThreadState.HasFlag(ThreadState.Stopped) || isStreamThreadDying) { - streamingThread?.Join(); + if (streamingThread != null && !streamingThread.Join(1000)) + { + DebugConsole.ThrowError("Sound stream thread join timed out!"); + } areStreamsPlaying = true; streamingThread = new Thread(UpdateStreaming) { @@ -639,10 +642,7 @@ namespace Barotrauma.Sounds bool killThread = false; while (!killThread) { - lock (threadDeathMutex) - { - areStreamsPlaying = false; - } + killThread = true; for (int i = 0; i < playingChannels.Length; i++) { lock (playingChannels[i]) @@ -654,10 +654,7 @@ namespace Barotrauma.Sounds { if (playingChannels[i][j].IsPlaying) { - lock (threadDeathMutex) - { - areStreamsPlaying = true; - } + killThread = false; playingChannels[i][j].UpdateStream(); } else @@ -678,7 +675,7 @@ namespace Barotrauma.Sounds } lock (threadDeathMutex) { - killThread = !areStreamsPlaying; + areStreamsPlaying = !killThread; } Thread.Sleep(10); //TODO: use a separate thread for network audio? } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index 9a8404fbf..53ae31dd9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -3,7 +3,7 @@ using Barotrauma.Sounds; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; @@ -73,8 +73,8 @@ namespace Barotrauma private static float updateMusicTimer; //ambience - private static List waterAmbiences = new List(); - private static SoundChannel[] waterAmbienceChannels = new SoundChannel[2]; + private static Sound waterAmbienceIn, waterAmbienceOut, waterAmbienceMoving; + private static SoundChannel[] waterAmbienceChannels = new SoundChannel[3]; private static float ambientSoundTimer; private static Vector2 ambientSoundInterval = new Vector2(20.0f, 40.0f); //x = min, y = max @@ -148,7 +148,7 @@ namespace Barotrauma } soundElements.AddRange(mainElement.Elements()); } - + SoundCount = 1 + soundElements.Count(); var startUpSoundElement = soundElements.Find(e => e.Name.ToString().Equals("startupsound", StringComparison.OrdinalIgnoreCase)); @@ -159,11 +159,13 @@ namespace Barotrauma } yield return CoroutineStatus.Running; - + List> miscSoundList = new List>(); - damageSounds = damageSounds ?? new List(); - musicClips = musicClips ?? new List(); - + damageSounds ??= new List(); + musicClips ??= new List(); + + bool firstWaterAmbienceLoaded = false; + foreach (XElement soundElement in soundElements) { yield return CoroutineStatus.Running; @@ -195,17 +197,57 @@ namespace Barotrauma FlowSounds.AddIfNotNull(GameMain.SoundManager.LoadSound(soundElement, false)); break; case "waterambience": - waterAmbiences.AddIfNotNull(GameMain.SoundManager.LoadSound(soundElement, false)); + //backwards compatibility (1st waterambience used to be played both inside and outside, 2nd when moving) + if (!firstWaterAmbienceLoaded) + { + waterAmbienceIn?.Dispose(); + waterAmbienceOut?.Dispose(); + if (File.Exists(soundElement.GetAttributeString("file", ""))) + { + waterAmbienceIn = GameMain.SoundManager.LoadSound(soundElement, false); + waterAmbienceOut = GameMain.SoundManager.LoadSound(soundElement, false); + } + else + { + waterAmbienceIn = GameMain.SoundManager.LoadSound(soundElement, false, "Content/Sounds/Water/WaterAmbienceIn.ogg"); + waterAmbienceOut = GameMain.SoundManager.LoadSound(soundElement, false, "Content/Sounds/Water/WaterAmbienceOut.ogg"); + } + firstWaterAmbienceLoaded = true; + } + else + { + waterAmbienceMoving?.Dispose(); + if (File.Exists(soundElement.GetAttributeString("file", ""))) + { + waterAmbienceMoving = GameMain.SoundManager.LoadSound(soundElement, false); + } + else + { + waterAmbienceMoving = GameMain.SoundManager.LoadSound(soundElement, false, "Content/Sounds/Water/WaterAmbienceMoving.ogg"); + } + } + break; + case "waterambiencein": + waterAmbienceIn?.Dispose(); + waterAmbienceIn = GameMain.SoundManager.LoadSound(soundElement, false); + break; + case "waterambienceout": + waterAmbienceOut?.Dispose(); + waterAmbienceOut = GameMain.SoundManager.LoadSound(soundElement, false); + break; + case "waterambiencemoving": + waterAmbienceMoving?.Dispose(); + waterAmbienceMoving = GameMain.SoundManager.LoadSound(soundElement, false); break; case "damagesound": Sound damageSound = GameMain.SoundManager.LoadSound(soundElement, false); if (damageSound == null) { continue; } - + string damageSoundType = soundElement.GetAttributeString("damagesoundtype", "None"); damageSounds.Add(new DamageSound( - damageSound, - soundElement.GetAttributeVector2("damagerange", Vector2.Zero), - damageSoundType, + damageSound, + soundElement.GetAttributeVector2("damagerange", Vector2.Zero), + damageSoundType, soundElement.GetAttributeString("requiredtag", ""))); break; @@ -218,15 +260,15 @@ namespace Barotrauma break; } } - catch (FileNotFoundException e) + catch (System.IO.FileNotFoundException e) { DebugConsole.ThrowError("Error while initializing SoundPlayer.", e); - } + } } musicClips.RemoveAll(mc => !soundElements.Any(e => SoundElementsEquivalent(mc.Element, e))); - for (int i=0;i mc.File == currentMusic[i].Filename)) { @@ -246,12 +288,6 @@ namespace Barotrauma }); FlowSounds.RemoveAll(s => s.Disposed); - waterAmbiences.ForEach(s => - { - if (!soundElements.Any(e => SoundElementsEquivalent(s.XElement, e))) { s.Dispose(); } - }); - waterAmbiences.RemoveAll(s => s.Disposed); - damageSounds.ForEach(s => { if (!soundElements.Any(e => SoundElementsEquivalent(s.sound.XElement, e))) { s.sound.Dispose(); } @@ -273,7 +309,7 @@ namespace Barotrauma fireVolumeLeft = new float[2]; fireVolumeRight = new float[2]; - miscSounds = miscSoundList.ToLookup(kvp => kvp.Key, kvp => kvp.Value); + miscSounds = miscSoundList.ToLookup(kvp => kvp.Key, kvp => kvp.Value); Initialized = true; @@ -282,7 +318,7 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - + public static void Update(float deltaTime) { @@ -293,27 +329,27 @@ namespace Barotrauma if (startUpSound != null && !GameMain.SoundManager.IsPlaying(startUpSound)) { startUpSound.Dispose(); - startUpSound = null; + startUpSound = null; } //stop water sounds if no sub is loaded - if (Submarine.MainSub == null || Screen.Selected != GameMain.GameScreen) + if (Submarine.MainSub == null || Screen.Selected != GameMain.GameScreen) { for (int i = 0; i < waterAmbienceChannels.Length; i++) { - if (waterAmbienceChannels[i] == null) continue; + if (waterAmbienceChannels[i] == null) { continue; } waterAmbienceChannels[i].FadeOutAndDispose(); waterAmbienceChannels[i] = null; } for (int i = 0; i < FlowSounds.Count; i++) { - if (flowSoundChannels[i] == null) continue; + if (flowSoundChannels[i] == null) { continue; } flowSoundChannels[i].FadeOutAndDispose(); flowSoundChannels[i] = null; } for (int i = 0; i < fireSoundChannels.Length; i++) { - if (fireSoundChannels[i] == null) continue; + if (fireSoundChannels[i] == null) { continue; } fireSoundChannels[i].FadeOutAndDispose(); fireSoundChannels[i] = null; } @@ -333,17 +369,20 @@ namespace Barotrauma } } - UpdateWaterAmbience(ambienceVolume); + UpdateWaterAmbience(ambienceVolume, deltaTime); UpdateWaterFlowSounds(deltaTime); UpdateRandomAmbience(deltaTime); - UpdateFireSounds(deltaTime); + UpdateFireSounds(deltaTime); } - private static void UpdateWaterAmbience(float ambienceVolume) + private static void UpdateWaterAmbience(float ambienceVolume, float deltaTime) { + if (GameMain.SoundManager.Disabled) { return; } + //how fast the sub is moving, scaled to 0.0 -> 1.0 float movementSoundVolume = 0.0f; + float insideSubFactor = 0.0f; foreach (Submarine sub in Submarine.Loaded) { float movementFactor = (sub.Velocity == Vector2.Zero) ? 0.0f : sub.Velocity.Length() / 10.0f; @@ -352,7 +391,12 @@ namespace Barotrauma if (Character.Controlled == null || Character.Controlled.Submarine != sub) { float dist = Vector2.Distance(GameMain.GameScreen.Cam.WorldViewCenter, sub.WorldPosition); - movementFactor = movementFactor / Math.Max(dist / 1000.0f, 1.0f); + movementFactor /= Math.Max(dist / 1000.0f, 1.0f); + insideSubFactor = Math.Max(1.0f / Math.Max(dist / 1000.0f, 1.0f), insideSubFactor); + } + else + { + insideSubFactor = 1.0f; } movementSoundVolume = Math.Max(movementSoundVolume, movementFactor); @@ -365,28 +409,38 @@ namespace Barotrauma } } - if (waterAmbiences.Count > 1) + for (int i = 0; i < 3; i++) { - if (waterAmbienceChannels[0] == null || !waterAmbienceChannels[0].IsPlaying) + float volume = 0.0f; + Sound sound = null; + switch (i) { - waterAmbienceChannels[0] = waterAmbiences[0].Play(ambienceVolume * (1.0f - movementSoundVolume),"waterambience"); - //waterAmbiences[0].Loop(waterAmbienceIndexes[0], ambienceVolume * (1.0f - movementSoundVolume)); - waterAmbienceChannels[0].Looping = true; - } - else - { - waterAmbienceChannels[0].Gain = ambienceVolume * (1.0f - movementSoundVolume); + case 0: + volume = ambienceVolume * (1.0f - movementSoundVolume) * insideSubFactor; + sound = waterAmbienceIn; + break; + case 1: + volume = ambienceVolume * movementSoundVolume * insideSubFactor; + sound = waterAmbienceMoving; + break; + case 2: + volume = 1.0f - insideSubFactor; + sound = waterAmbienceOut; + break; } - if (waterAmbienceChannels[1] == null || !waterAmbienceChannels[1].IsPlaying) + if ((waterAmbienceChannels[i] == null || !waterAmbienceChannels[i].IsPlaying) && volume > 0.01f) { - waterAmbienceChannels[1] = waterAmbiences[1].Play(ambienceVolume * movementSoundVolume, "waterambience"); - //waterAmbienceIndexes[1] = waterAmbiences[1].Loop(waterAmbienceIndexes[1], ambienceVolume * movementSoundVolume); - waterAmbienceChannels[1].Looping = true; + waterAmbienceChannels[i] = sound.Play(volume, "waterambience"); + waterAmbienceChannels[i].Looping = true; } - else + else if (waterAmbienceChannels[i] != null) { - waterAmbienceChannels[1].Gain = ambienceVolume * movementSoundVolume; + waterAmbienceChannels[i].Gain += deltaTime * Math.Sign(volume - waterAmbienceChannels[i].Gain); + if (waterAmbienceChannels[i].Gain < 0.01f) + { + waterAmbienceChannels[i].FadeOutAndDispose(); + } } } } @@ -401,17 +455,25 @@ namespace Barotrauma Vector2 listenerPos = new Vector2(GameMain.SoundManager.ListenerPosition.X, GameMain.SoundManager.ListenerPosition.Y); foreach (Gap gap in Gap.GapList) { - if (gap.Open < 0.01f) continue; - float gapFlow = Math.Abs(gap.LerpedFlowForce.X) + Math.Abs(gap.LerpedFlowForce.Y) * 2.5f; - - if (gapFlow < 10.0f) continue; - - int flowSoundIndex = (int)Math.Floor(MathHelper.Clamp(gapFlow / MaxFlowStrength, 0, FlowSounds.Count)); - flowSoundIndex = Math.Min(flowSoundIndex, FlowSounds.Count - 1); - Vector2 diff = gap.WorldPosition - listenerPos; if (Math.Abs(diff.X) < FlowSoundRange && Math.Abs(diff.Y) < FlowSoundRange) { + if (gap.Open < 0.01f) { continue; } + float gapFlow = Math.Abs(gap.LerpedFlowForce.X) + Math.Abs(gap.LerpedFlowForce.Y) * 2.5f; + if (!gap.IsRoomToRoom) { gapFlow *= 2.0f; } + if (gapFlow < 10.0f) { continue; } + + if (gap.linkedTo.Count == 2 && gap.linkedTo[0] is Hull hull1 && gap.linkedTo[1] is Hull hull2) + { + //no flow sounds between linked hulls (= rooms consisting of multiple hulls) + if (hull1.linkedTo.Contains(hull2)) { continue; } + if (hull1.linkedTo.Any(h => h.linkedTo.Contains(hull1) && h.linkedTo.Contains(hull2))) { continue; } + if (hull2.linkedTo.Any(h => h.linkedTo.Contains(hull1) && h.linkedTo.Contains(hull2))) { continue; } + } + + int flowSoundIndex = (int)Math.Floor(MathHelper.Clamp(gapFlow / MaxFlowStrength, 0, FlowSounds.Count)); + flowSoundIndex = Math.Min(flowSoundIndex, FlowSounds.Count - 1); + float dist = diff.Length(); float distFallOff = dist / FlowSoundRange; if (distFallOff >= 0.99f) continue; @@ -419,11 +481,11 @@ namespace Barotrauma //flow at the left side if (diff.X < 0) { - targetFlowLeft[flowSoundIndex] = 1.0f - distFallOff; + targetFlowLeft[flowSoundIndex] += 1.0f - distFallOff; } else { - targetFlowRight[flowSoundIndex] = 1.0f - distFallOff; + targetFlowRight[flowSoundIndex] += 1.0f - distFallOff; } } } @@ -432,10 +494,10 @@ namespace Barotrauma { flowVolumeLeft[i] = (targetFlowLeft[i] < flowVolumeLeft[i]) ? Math.Max(targetFlowLeft[i], flowVolumeLeft[i] - deltaTime) : - Math.Min(targetFlowLeft[i], flowVolumeLeft[i] + deltaTime); + Math.Min(targetFlowLeft[i], flowVolumeLeft[i] + deltaTime * 10.0f); flowVolumeRight[i] = (targetFlowRight[i] < flowVolumeRight[i]) ? Math.Max(targetFlowRight[i], flowVolumeRight[i] - deltaTime) : - Math.Min(targetFlowRight[i], flowVolumeRight[i] + deltaTime); + Math.Min(targetFlowRight[i], flowVolumeRight[i] + deltaTime * 10.0f); if (flowVolumeLeft[i] < 0.05f && flowVolumeRight[i] < 0.05f) { @@ -869,20 +931,22 @@ namespace Barotrauma PlayDamageSound(damageType, damage, bodyPosition, 800.0f); } + private static readonly List tempList = new List(); public static void PlayDamageSound(string damageType, float damage, Vector2 position, float range = 2000.0f, IEnumerable tags = null) { damage = MathHelper.Clamp(damage + Rand.Range(-10.0f, 10.0f), 0.0f, 100.0f); - var sounds = damageSounds.FindAll(s => - (s.damageRange == Vector2.Zero || - (damage >= s.damageRange.X && damage <= s.damageRange.Y)) && - s.damageType == damageType && - (tags == null ? string.IsNullOrEmpty(s.requiredTag) : tags.Contains(s.requiredTag))); - - if (!sounds.Any()) return; - - int selectedSound = Rand.Int(sounds.Count); - sounds[selectedSound].sound.Play(1.0f, range, position, muffle: ShouldMuffleSound(Character.Controlled, position, range, null)); - } - + tempList.Clear(); + foreach (var s in damageSounds) + { + if ((s.damageRange == Vector2.Zero || + (damage >= s.damageRange.X && damage <= s.damageRange.Y)) && + string.Equals(s.damageType, damageType, StringComparison.OrdinalIgnoreCase) && + (tags == null ? string.IsNullOrEmpty(s.requiredTag) : tags.Contains(s.requiredTag))) + { + tempList.Add(s); + } + } + tempList.GetRandom().sound?.Play(1.0f, range, position, muffle: ShouldMuffleSound(Character.Controlled, position, range, null)); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs index 974a67cdd..734925a07 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs @@ -44,13 +44,15 @@ namespace Barotrauma.Sounds new BandpassFilter(VoipConfig.FREQUENCY, 2000) }; + private float gain; public float Gain { - get { return soundChannel == null ? 0.0f : soundChannel.Gain; } + get { return soundChannel == null ? 0.0f : gain; } set { if (soundChannel == null) { return; } - soundChannel.Gain = value; + gain = value; + soundChannel.Gain = value * GameMain.Config.VoiceChatVolume; } } @@ -75,6 +77,7 @@ namespace Barotrauma.Sounds SoundChannel chn = new SoundChannel(this, 1.0f, null, 0.4f, 1.0f, "voip", false); soundChannel = chn; + Gain = 1.0f; } public override float GetAmplitudeAtPlaybackPos(int playbackPos) @@ -98,6 +101,12 @@ namespace Barotrauma.Sounds for (int i = 0; i < readSamples; i++) { float fVal = ShortToFloat(buffer[i]); + + if (gain * GameMain.Config.VoiceChatVolume > 1.0f) //TODO: take distance into account? + { + fVal = Math.Clamp(fVal * gain * GameMain.Config.VoiceChatVolume, -1.0f, 1.0f); + } + if (UseMuffleFilter) { foreach (var filter in muffleFilters) @@ -114,19 +123,6 @@ namespace Barotrauma.Sounds } buffer[i] = FloatToShort(fVal); } - if (UseMuffleFilter) - { - ApplyFilters(muffleFilters, buffer, readSamples); - } - - if (UseRadioFilter) - { - ApplyFilters(radioFilters, buffer, readSamples); - } - } - - private void ApplyFilters(IEnumerable filters, short[] buffer, int readSamples) - { } public override SoundChannel Play(float gain, float range, Vector2 position, bool muffle = false) @@ -152,13 +148,21 @@ namespace Barotrauma.Sounds public override int FillStreamBuffer(int samplePos, short[] buffer) { queue.RetrieveBuffer(bufferID, out int compressedSize, out byte[] compressedBuffer); - if (compressedSize > 0) + try { - VoipConfig.Decoder.Decode(compressedBuffer, 0, compressedSize, buffer, 0, VoipConfig.BUFFER_SIZE); - bufferID++; - return VoipConfig.BUFFER_SIZE * 2; + if (compressedSize > 0) + { + VoipConfig.Decoder.Decode(compressedBuffer, 0, compressedSize, buffer, 0, VoipConfig.BUFFER_SIZE); + bufferID++; + return VoipConfig.BUFFER_SIZE * 2; + } + if (bufferID < queue.LatestBufferID - (VoipQueue.BUFFER_COUNT - 1)) bufferID = queue.LatestBufferID - (VoipQueue.BUFFER_COUNT - 1); + } + catch (Exception e) + { + DebugConsole.ThrowError($"Failed to decode Opus buffer (buffer size {compressedBuffer.Length}, packet size {compressedSize})", e); + bufferID = queue.LatestBufferID - (VoipQueue.BUFFER_COUNT - 1); } - if (bufferID < queue.LatestBufferID - (VoipQueue.BUFFER_COUNT - 1)) bufferID = queue.LatestBufferID - (VoipQueue.BUFFER_COUNT - 1); return 0; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs index 8c9c45a86..64fc96a80 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Xml.Linq; -using SpriteParams = Barotrauma.RagdollParams.SpriteParams; namespace Barotrauma { @@ -64,6 +63,14 @@ namespace Barotrauma } } + private float scale; + [Serialize(1.0f, true), Editable] + public float Scale + { + get { return scale; } + private set { scale = MathHelper.Clamp(value, 0.0f, 10.0f); } + } + [Serialize(AnimationType.None, false), Editable] public AnimationType RotationAnim { get; private set; } @@ -73,6 +80,9 @@ namespace Barotrauma [Serialize(0, false, description: "If > 0, only one sprite of the same group is used (chosen randomly)"), Editable(ReadOnly = true)] public int RandomGroupID { get; private set; } + [Serialize("1.0,1.0,1.0,1.0", true), Editable()] + public Color Color { get; set; } + /// /// The sprite is only drawn if these conditions are fulfilled /// @@ -158,7 +168,7 @@ namespace Barotrauma { foreach (int spriteGroup in spriteGroups.Keys) { - for (int i = 0; i < spriteGroups.Count; i++) + for (int i = 0; i < spriteGroups[spriteGroup].Count; i++) { var decorativeSprite = spriteGroups[spriteGroup][i]; if (decorativeSprite == null) { continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformableSprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformableSprite.cs index 6d54711f8..bde2b3cb8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformableSprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformableSprite.cs @@ -33,10 +33,12 @@ namespace Barotrauma get { return effect; } } + public bool Invert { get; set; } + private Point spritePos; private Point spriteSize; - partial void InitProjSpecific(XElement element, int? subdivisionsX, int? subdivisionsY, bool lazyLoad) + partial void InitProjSpecific(XElement element, int? subdivisionsX, int? subdivisionsY, bool lazyLoad, bool invert) { if (effect == null) { @@ -47,6 +49,8 @@ namespace Barotrauma effect = GameMain.Instance.Content.Load("Effects/deformshader_opengl"); #endif } + + Invert = invert; //use subdivisions configured in the xml if the arguments passed to the method are null Vector2 subdivisionsInXml = element.GetAttributeVector2("subdivisions", Vector2.One); @@ -121,6 +125,12 @@ namespace Barotrauma uvBottomRight = Vector2.Divide((pos + size).ToVector2(), textureSize); uvTopLeftFlipped = Vector2.Divide(new Vector2(pos.X + size.X, pos.Y), textureSize); uvBottomRightFlipped = Vector2.Divide(new Vector2(pos.X, pos.Y + size.Y), textureSize); + if (Invert) + { + var temp = uvBottomRightFlipped; + uvBottomRightFlipped = uvTopLeftFlipped; + uvTopLeftFlipped = temp; + } for (int i = 0; i < 2; i++) { @@ -267,7 +277,7 @@ namespace Barotrauma Matrix.CreateTranslation(pos); } - public void Draw(Camera cam, Vector3 pos, Vector2 origin, float rotate, Vector2 scale, Color color, bool flip = false, bool mirror = false) + public void Draw(Camera cam, Vector3 pos, Vector2 origin, float rotate, Vector2 scale, Color color, bool mirror = false, bool invert = false) { if (Sprite.Texture == null) { return; } if (!initialized) { Init(); } @@ -291,13 +301,13 @@ namespace Barotrauma effect.Parameters["deformArray"].SetValue(deformAmount); effect.Parameters["deformArrayWidth"].SetValue(deformArrayWidth); effect.Parameters["deformArrayHeight"].SetValue(deformArrayHeight); - if (mirror) + if (invert) { - flip = !flip; + mirror = !mirror; } - effect.Parameters["uvTopLeft"].SetValue(flip ? uvTopLeftFlipped : uvTopLeft); - effect.Parameters["uvBottomRight"].SetValue(flip ? uvBottomRightFlipped : uvBottomRight); - effect.GraphicsDevice.SetVertexBuffer(flip ? flippedVertexBuffer : vertexBuffer); + effect.Parameters["uvTopLeft"].SetValue(mirror ? uvTopLeftFlipped : uvTopLeft); + effect.Parameters["uvBottomRight"].SetValue(mirror ? uvBottomRightFlipped : uvBottomRight); + effect.GraphicsDevice.SetVertexBuffer(mirror ? flippedVertexBuffer : vertexBuffer); effect.GraphicsDevice.Indices = indexBuffer; effect.CurrentTechnique.Passes[0].Apply(); effect.GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, triangleCount); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs index c7beda481..a64df8a01 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs @@ -1,7 +1,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Collections.Generic; @@ -21,6 +21,11 @@ namespace Barotrauma } } + public bool Loaded + { + get { return texture != null && !cannotBeLoaded; } + } + public Sprite(Texture2D texture, Rectangle? sourceRectangle, Vector2? newOffset, float newRotation = 0.0f) { this.texture = texture; @@ -42,7 +47,12 @@ namespace Barotrauma partial void LoadTexture(ref Vector4 sourceVector, ref bool shouldReturn) { - texture = LoadTexture(this.FilePath); + texture = LoadTexture(this.FilePath, out Sprite reusedSprite, Compress); + if (reusedSprite != null) + { + FilePath = string.Intern(reusedSprite.FilePath); + FullPath = string.Intern(reusedSprite.FullPath); + } if (texture == null) { @@ -56,11 +66,25 @@ namespace Barotrauma public void EnsureLazyLoaded() { - if (!lazyLoad || texture != null || cannotBeLoaded) { return; } + if (!LazyLoad || texture != null || cannotBeLoaded) { return; } Vector4 sourceVector = Vector4.Zero; bool temp2 = false; - LoadTexture(ref sourceVector, ref temp2); + int maxLoadRetries = 3; + for (int i = 0; i <= maxLoadRetries; i++) + { + try + { + LoadTexture(ref sourceVector, ref temp2); + } + catch (System.IO.IOException) + { + if (i == maxLoadRetries || !File.Exists(FilePath)) { throw; } + DebugConsole.NewMessage("Loading sprite \"" + FilePath + "\" failed, retrying in 250 ms..."); + System.Threading.Thread.Sleep(500); + } + } + if (sourceRect.Width == 0 && sourceRect.Height == 0) { sourceRect = new Rectangle((int)sourceVector.X, (int)sourceVector.Y, (int)sourceVector.Z, (int)sourceVector.W); @@ -69,11 +93,6 @@ namespace Barotrauma size.Y *= sourceRect.Height; RelativeOrigin = SourceElement.GetAttributeVector2("origin", new Vector2(0.5f, 0.5f)); } - foreach (Sprite s in LoadedSprites) - { - if (s == this) { continue; } - if (s.FullPath == FullPath && s.texture != null) { s.texture = texture; } - } if (texture == null) { cannotBeLoaded = true; @@ -85,7 +104,7 @@ namespace Barotrauma public void ReloadTexture(IEnumerable spritesToUpdate) { texture.Dispose(); - texture = TextureLoader.FromFile(FilePath); + texture = TextureLoader.FromFile(FilePath, Compress); foreach (Sprite sprite in spritesToUpdate) { sprite.texture = texture; @@ -99,6 +118,12 @@ namespace Barotrauma public static Texture2D LoadTexture(string file) { + return LoadTexture(file, out _); + } + + public static Texture2D LoadTexture(string file, out Sprite reusedSprite, bool compress = true) + { + reusedSprite = null; if (string.IsNullOrWhiteSpace(file)) { Texture2D t = null; @@ -111,13 +136,17 @@ namespace Barotrauma file = Path.GetFullPath(file); foreach (Sprite s in LoadedSprites) { - if (s.FullPath == file && s.texture != null && !s.texture.IsDisposed) { return s.texture; } + if (s.FullPath == file && s.texture != null && !s.texture.IsDisposed) + { + reusedSprite = s; + return s.texture; + } } if (File.Exists(file)) { ToolBox.IsProperFilenameCase(file); - return TextureLoader.FromFile(file); + return TextureLoader.FromFile(file, compress); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionPrefab.cs index 42d8d7f84..deed55665 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionPrefab.cs @@ -17,6 +17,7 @@ namespace Barotrauma public static void Init() { + List.Clear(); var files = GameMain.Instance.GetFilesOfType(ContentType.TraitorMissions); foreach (ContentFile file in files) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/HttpEncoder.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/HttpEncoder.cs index f6affb2cb..f6c015be6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/HttpEncoder.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/HttpEncoder.cs @@ -34,7 +34,7 @@ using System; using System.Collections.Generic; using System.Configuration; using System.Globalization; -using System.IO; +using Barotrauma.IO; using System.Text; #if NET_4_0 using System.Web.Configuration; @@ -225,7 +225,7 @@ namespace RestSharp.Contrib if (String.IsNullOrEmpty(value)) return value; - MemoryStream result = new MemoryStream(); + System.IO.MemoryStream result = new System.IO.MemoryStream(); int length = value.Length; for (int i = 0; i < length; i++) UrlPathEncodeChar(value[i], result); @@ -248,7 +248,7 @@ namespace RestSharp.Contrib if (count < 0 || count > blen - offset) throw new ArgumentOutOfRangeException("count"); - MemoryStream result = new MemoryStream(count); + System.IO.MemoryStream result = new System.IO.MemoryStream(count); int end = offset + count; for (int i = offset; i < end; i++) UrlEncodeChar((char)bytes[i], result, false); @@ -575,7 +575,7 @@ namespace RestSharp.Contrib ); } - internal static void UrlEncodeChar(char c, Stream result, bool isUnicode) + internal static void UrlEncodeChar(char c, System.IO.Stream result, bool isUnicode) { if (c > 255) { @@ -632,7 +632,7 @@ namespace RestSharp.Contrib result.WriteByte((byte)c); } - internal static void UrlPathEncodeChar(char c, Stream result) + internal static void UrlPathEncodeChar(char c, System.IO.Stream result) { if (c < 33 || c > 126) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/HttpUtility.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/HttpUtility.cs index 1ba481dc2..9f0ee6910 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/HttpUtility.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/HttpUtility.cs @@ -34,7 +34,7 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; -using System.IO; +using Barotrauma.IO; using System.Security.Permissions; using System.Text; @@ -76,6 +76,7 @@ namespace RestSharp.Contrib #region Methods + /* public static void HtmlAttributeEncode(string s, TextWriter output) { if (output == null) @@ -92,6 +93,7 @@ namespace RestSharp.Contrib output.Write(HttpEncoder.HtmlAttributeEncode(s)); #endif } + */ public static string HtmlAttributeEncode(string s) { @@ -113,7 +115,7 @@ namespace RestSharp.Contrib return UrlDecode(str, Encoding.UTF8); } - static char[] GetChars(MemoryStream b, Encoding e) + static char[] GetChars(System.IO.MemoryStream b, Encoding e) { return e.GetChars(b.GetBuffer(), 0, (int)b.Length); } @@ -260,7 +262,7 @@ namespace RestSharp.Contrib throw new ArgumentOutOfRangeException("count"); StringBuilder output = new StringBuilder(); - MemoryStream acc = new MemoryStream(); + System.IO.MemoryStream acc = new System.IO.MemoryStream(); int end = count + offset; int xchar; @@ -354,7 +356,7 @@ namespace RestSharp.Contrib if (count < 0 || offset > len - count) throw new ArgumentOutOfRangeException("count"); - MemoryStream result = new MemoryStream(); + System.IO.MemoryStream result = new System.IO.MemoryStream(); int end = offset + count; for (int i = offset; i < end; i++) { @@ -492,7 +494,7 @@ namespace RestSharp.Contrib if (str.Length == 0) return new byte[0]; - MemoryStream result = new MemoryStream(str.Length); + System.IO.MemoryStream result = new System.IO.MemoryStream(str.Length); foreach (char c in str) { HttpEncoder.UrlEncodeChar(c, result, true); @@ -525,6 +527,7 @@ namespace RestSharp.Contrib /// /// The HTML string to decode /// The TextWriter output stream containing the decoded string. + /* public static void HtmlDecode(string s, TextWriter output) { if (output == null) @@ -545,6 +548,7 @@ namespace RestSharp.Contrib #endif } } + */ public static string HtmlEncode(string s) { @@ -566,6 +570,7 @@ namespace RestSharp.Contrib /// /// The string to encode. /// The TextWriter output stream containing the encoded string. + /* public static void HtmlEncode(string s, TextWriter output) { if (output == null) @@ -586,6 +591,8 @@ namespace RestSharp.Contrib #endif } } + */ + #if NET_4_0 public static string HtmlEncode (object value) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs index d67ad68b2..f98cdc0dc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/LocalizationCSVtoXML.cs @@ -1,7 +1,7 @@ #if DEBUG using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Text; using System.Text.RegularExpressions; using System.Linq; @@ -41,7 +41,7 @@ namespace Barotrauma if (Directory.Exists(conversationsPath + $"/{languageNoWhitespace}")) { - string[] conversationFileArray = Directory.GetFiles(conversationsPath + $"/{languageNoWhitespace}", "*.csv", SearchOption.AllDirectories); + IEnumerable conversationFileArray = Directory.GetFiles(conversationsPath + $"/{languageNoWhitespace}", "*.csv", System.IO.SearchOption.AllDirectories); if (conversationFileArray != null) { @@ -58,7 +58,7 @@ namespace Barotrauma if (Directory.Exists(infoTextPath + $"/{languageNoWhitespace}")) { - string[] infoTextFileArray = Directory.GetFiles(infoTextPath + $"/{languageNoWhitespace}", "*.csv", SearchOption.AllDirectories); + IEnumerable infoTextFileArray = Directory.GetFiles(infoTextPath + $"/{languageNoWhitespace}", "*.csv", System.IO.SearchOption.AllDirectories); if (infoTextFileArray != null) { @@ -145,7 +145,7 @@ namespace Barotrauma } else if (split[0].Contains(".") && !split[0].Any(char.IsUpper)) // An empty field { - xmlContent.Add($"<{split[0]}>"); + xmlContent.Add($"<{split[0]}>"); } else // A header { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/Quad.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/Quad.cs new file mode 100644 index 000000000..b373c9e7e --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/Quad.cs @@ -0,0 +1,67 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Barotrauma +{ + static class Quad + { + private static VertexBuffer vertexBuffer = null; + private static IndexBuffer indexBuffer = null; + private static BasicEffect basicEffect = null; + private static GraphicsDevice graphicsDevice = null; + + public static void Init(GraphicsDevice graphics) + { + if (graphicsDevice != null) { return; } + + graphicsDevice = graphics; + + vertexBuffer = new VertexBuffer(graphics, VertexPositionTexture.VertexDeclaration, 4, BufferUsage.WriteOnly); + indexBuffer = new IndexBuffer(graphics, IndexElementSize.SixteenBits, 4, BufferUsage.WriteOnly); + + InitVertexData(); + indexBuffer.SetData(new ushort[] { 0, 1, 2, 3 }); + + basicEffect = new BasicEffect(graphics) { TextureEnabled = true }; + + GameMain.Instance.OnResolutionChanged += () => + { + InitVertexData(); + }; + } + + private static void InitVertexData() + { + Vector2 halfPixelOffset = Vector2.Zero; +#if LINUX || OSX + halfPixelOffset = new Vector2(0.5f / GameMain.GraphicsWidth, 0.5f / GameMain.GraphicsHeight); +#endif + + VertexPositionTexture[] vertices = + { + new VertexPositionTexture(new Vector3(-1f, -1f, 1f), new Vector2(0f, 1f) + halfPixelOffset), + new VertexPositionTexture(new Vector3(-1f, 1f, 1f), new Vector2(0f, 0f) + halfPixelOffset), + new VertexPositionTexture(new Vector3(1f, -1f, 1f), new Vector2(1f, 1f) + halfPixelOffset), + new VertexPositionTexture(new Vector3(1f, 1f, 1f), new Vector2(1f, 0f) + halfPixelOffset) + }; + + vertexBuffer.SetData(vertices); + } + + public static void UseBasicEffect(Texture2D texture) + { + basicEffect.Texture = texture; + basicEffect.CurrentTechnique.Passes[0].Apply(); + } + + public static void Render() + { + graphicsDevice.SetVertexBuffer(vertexBuffer); + graphicsDevice.Indices = indexBuffer; + graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleStrip, 0, 0, 2); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs index 05e0e93dc..e729ecb33 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/TextureLoader.cs @@ -1,7 +1,9 @@ +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; -using System.IO; +using Barotrauma.IO; using System.Threading.Tasks; +using Lidgren.Network; using Color = Microsoft.Xna.Framework.Color; namespace Barotrauma @@ -34,36 +36,186 @@ namespace Barotrauma }); } - public static Texture2D FromFile(string path, bool mipmap=false) + private static byte[] CompressDxt5(byte[] data, int width, int height) { - path = path.CleanUpPath(); - try + using (System.IO.MemoryStream mstream = new System.IO.MemoryStream()) { - using (Stream fileStream = File.OpenRead(path)) + for (int y = 0; y < height; y += 4) { - return FromStream(fileStream, path, mipmap); + for (int x = 0; x < width; x += 4) + { + int offset = x * 4 + y * 4 * width; + CompressDxt5Block(data, offset, width, mstream); + } } - - } - catch (Exception e) - { - DebugConsole.ThrowError("Loading texture \"" + path + "\" failed!", e); - return null; + return mstream.ToArray(); } } - public static Texture2D FromStream(Stream fileStream, string path=null, bool mipmap=false) + private static void CompressDxt5Block(byte[] data, int offset, int width, System.IO.Stream output) + { + int r1 = 255, g1 = 255, b1 = 255, a1 = 255; + int r2 = 0, g2 = 0, b2 = 0, a2 = 0; + + //determine the two colors to interpolate between: + //color 1 represents lowest luma, color 2 represents highest luma + for (int i = 0; i < 16; i++) + { + int pixelOffset = offset + (4 * ((i % 4) + (width * (i >> 2)))); + int r, g, b, a; + r = data[pixelOffset + 0]; + g = data[pixelOffset + 1]; + b = data[pixelOffset + 2]; + a = data[pixelOffset + 3]; + if (r * 299 + g * 587 + b * 114 < r1 * 299 + g1 * 587 + b1 * 114) + { + r1 = r; g1 = g; b1 = b; + } + if (r * 299 + g * 587 + b * 114 > r2 * 299 + g2 * 587 + b2 * 114) + { + r2 = r; g2 = g; b2 = b; + } + if (a < a1) { a1 = a; } + if (a > a2) { a2 = a; } + } + + //convert the colors to rgb565 (16-bit rgb) + int r1_565 = (r1 * 0x1f) / 0xff; if (r1_565 > 0x1f) { r1_565 = 0x1f; } + int g1_565 = (g1 * 0x3f) / 0xff; if (g1_565 > 0x3f) { g1_565 = 0x3f; } + int b1_565 = (b1 * 0x1f) / 0xff; if (b1_565 > 0x1f) { b1_565 = 0x1f; } + + int r2_565 = (r2 * 0x1f) / 0xff; if (r2_565 > 0x1f) { r2_565 = 0x1f; } + int g2_565 = (g2 * 0x3f) / 0xff; if (g2_565 > 0x3f) { g2_565 = 0x3f; } + int b2_565 = (b2 * 0x1f) / 0xff; if (b2_565 > 0x1f) { b2_565 = 0x1f; } + + //luma is also used to determine which color on the palette + //most closely resembles each pixel to compress, so we + //calculate this here + int y1 = r1 * 299 + g1 * 587 + b1 * 114; + int y2 = r2 * 299 + g2 * 587 + b2 * 114; + + byte[] newData = new byte[16]; + for (int i = 0; i < 16; i++) + { + int pixelOffset = offset + (4 * ((i % 4) + (width * (i >> 2)))); + int r, g, b, a; + r = data[pixelOffset + 0]; + g = data[pixelOffset + 1]; + b = data[pixelOffset + 2]; + a = data[pixelOffset + 3]; + + if (a1 < a2) + { + a -= a1; + a = (a * 0x7) / (a2 - a1); + if (a > 0x7) { a = 0x7; } + + switch (a) + { + case 0: + a = 1; + break; + case 1: + a = 7; + break; + case 2: + a = 6; + break; + case 3: + a = 5; + break; + case 4: + a = 4; + break; + case 5: + a = 3; + break; + case 6: + a = 2; + break; + case 7: + a = 0; + break; + } + } + else + { + a = 0; + } + + NetBitWriter.WriteUInt32((uint)a, 3, newData, 16 + (i * 3)); + + int y = r * 299 + g * 587 + b * 114; + + int max = y2 - y1; + int diffY = y - y1; + + int paletteIndex; + if (diffY < max / 4) + { + paletteIndex = 0; + } + else if (diffY < max / 2) + { + paletteIndex = 2; + } + else if (diffY < max * 3 / 4) + { + paletteIndex = 3; + } + else + { + paletteIndex = 1; + } + newData[12 + (i / 4)] |= (byte)(paletteIndex << (2 * (i % 4))); + } + + newData[0] = (byte)a2; + newData[1] = (byte)a1; + + newData[9] = (byte)((r1_565 << 3) | (g1_565 >> 3)); + newData[8] = (byte)((g1_565 << 5) | b1_565); + newData[11] = (byte)((r2_565 << 3) | (g2_565 >> 3)); + newData[10] = (byte)((g2_565 << 5) | b2_565); + + output.Write(newData, 0, 16); + } + + public static Texture2D FromFile(string path, bool compress = true, bool mipmap = false) + { + using (FileStream fileStream = File.OpenRead(path)) + { + return FromStream(fileStream, path, compress, mipmap); + } + } + + public static Texture2D FromStream(System.IO.Stream stream, string path = null, bool compress = true, bool mipmap = false) { try { - int width = 0; int height = 0; int channels = 0; + path = path.CleanUpPath(); byte[] textureData = null; - textureData = Texture2D.TextureDataFromStream(fileStream, out width, out height, out channels); + textureData = Texture2D.TextureDataFromStream(stream, out int width, out int height, out int channels); + + SurfaceFormat format = SurfaceFormat.Color; + if (GameMain.Config.TextureCompressionEnabled && compress) + { + if (((width & 0x03) == 0) && ((height & 0x03) == 0)) + { + textureData = CompressDxt5(textureData, width, height); + format = SurfaceFormat.Dxt5; + mipmap = false; + } + else + { + DebugConsole.NewMessage($"Could not compress a texture because the dimensions are a multiple of 4 (path: {path ?? "null"}, size: {width}x{height})", Color.Orange); + } + } Texture2D tex = null; CrossThread.RequestExecutionOnMainThread(() => { - tex = new Texture2D(_graphicsDevice, width, height, mipmap, SurfaceFormat.Color); + tex = new Texture2D(_graphicsDevice, width, height, mipmap, format); tex.SetData(textureData); }); return tex; @@ -74,7 +226,8 @@ namespace Barotrauma if (e is SharpDX.SharpDXException) { throw; } #endif - DebugConsole.ThrowError("Loading texture from stream failed!", e); + DebugConsole.ThrowError(string.IsNullOrEmpty(path) ? "Loading texture from stream failed!" : + "Loading texture \"" + path + "\" failed!", e); return null; } } diff --git a/Barotrauma/BarotraumaClient/Content/Effects/deformshader.xnb b/Barotrauma/BarotraumaClient/Content/Effects/deformshader.xnb index 35b0ae81b..e40446c86 100644 Binary files a/Barotrauma/BarotraumaClient/Content/Effects/deformshader.xnb and b/Barotrauma/BarotraumaClient/Content/Effects/deformshader.xnb differ diff --git a/Barotrauma/BarotraumaClient/Content/Effects/deformshader_opengl.xnb b/Barotrauma/BarotraumaClient/Content/Effects/deformshader_opengl.xnb index 15bed2a78..507c1f1e3 100644 Binary files a/Barotrauma/BarotraumaClient/Content/Effects/deformshader_opengl.xnb and b/Barotrauma/BarotraumaClient/Content/Effects/deformshader_opengl.xnb differ diff --git a/Barotrauma/BarotraumaClient/Content/Effects/losshader.xnb b/Barotrauma/BarotraumaClient/Content/Effects/losshader.xnb index 3d0a76ecb..b17c38ef3 100644 Binary files a/Barotrauma/BarotraumaClient/Content/Effects/losshader.xnb and b/Barotrauma/BarotraumaClient/Content/Effects/losshader.xnb differ diff --git a/Barotrauma/BarotraumaClient/Content/Effects/losshader_opengl.xnb b/Barotrauma/BarotraumaClient/Content/Effects/losshader_opengl.xnb index 96032c202..bd4516465 100644 Binary files a/Barotrauma/BarotraumaClient/Content/Effects/losshader_opengl.xnb and b/Barotrauma/BarotraumaClient/Content/Effects/losshader_opengl.xnb differ diff --git a/Barotrauma/BarotraumaClient/Content/Effects/postprocess.xnb b/Barotrauma/BarotraumaClient/Content/Effects/postprocess.xnb index 91cf8a795..f55a97a59 100644 Binary files a/Barotrauma/BarotraumaClient/Content/Effects/postprocess.xnb and b/Barotrauma/BarotraumaClient/Content/Effects/postprocess.xnb differ diff --git a/Barotrauma/BarotraumaClient/Content/Effects/postprocess_opengl.xnb b/Barotrauma/BarotraumaClient/Content/Effects/postprocess_opengl.xnb index 8c8308fcc..acc1f0930 100644 Binary files a/Barotrauma/BarotraumaClient/Content/Effects/postprocess_opengl.xnb and b/Barotrauma/BarotraumaClient/Content/Effects/postprocess_opengl.xnb differ diff --git a/Barotrauma/BarotraumaClient/Content/Effects/solidcolor.xnb b/Barotrauma/BarotraumaClient/Content/Effects/solidcolor.xnb index 8d2819b44..469f0e2c4 100644 Binary files a/Barotrauma/BarotraumaClient/Content/Effects/solidcolor.xnb and b/Barotrauma/BarotraumaClient/Content/Effects/solidcolor.xnb differ diff --git a/Barotrauma/BarotraumaClient/Content/Effects/solidcolor_opengl.xnb b/Barotrauma/BarotraumaClient/Content/Effects/solidcolor_opengl.xnb index efbff6947..ab1d6c897 100644 Binary files a/Barotrauma/BarotraumaClient/Content/Effects/solidcolor_opengl.xnb and b/Barotrauma/BarotraumaClient/Content/Effects/solidcolor_opengl.xnb differ diff --git a/Barotrauma/BarotraumaClient/Content/Effects/watershader.xnb b/Barotrauma/BarotraumaClient/Content/Effects/watershader.xnb index 680d837b5..e5bd61ae4 100644 Binary files a/Barotrauma/BarotraumaClient/Content/Effects/watershader.xnb and b/Barotrauma/BarotraumaClient/Content/Effects/watershader.xnb differ diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 83a5aee27..c889cf39b 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.9.9.0 + 0.9.1000.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 7e1e8ffa1..80ac177e3 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.9.9.0 + 0.9.1000.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/Shaders/Content_opengl.mgcb b/Barotrauma/BarotraumaClient/Shaders/Content_opengl.mgcb index 2d201daec..e18949d5d 100644 --- a/Barotrauma/BarotraumaClient/Shaders/Content_opengl.mgcb +++ b/Barotrauma/BarotraumaClient/Shaders/Content_opengl.mgcb @@ -19,11 +19,6 @@ /processorParam:DebugMode=Auto /build:blurshader_opengl.fx -#begin Content_opengl.mgcb -/importer: -/processor: -/build:Content_opengl.mgcb - #begin damageshader_opengl.fx /importer:EffectImporter /processor:EffectProcessor diff --git a/Barotrauma/BarotraumaClient/Shaders/blurshader.fx b/Barotrauma/BarotraumaClient/Shaders/blurshader.fx index 45c4cd015..54a5f1e81 100644 --- a/Barotrauma/BarotraumaClient/Shaders/blurshader.fx +++ b/Barotrauma/BarotraumaClient/Shaders/blurshader.fx @@ -10,7 +10,7 @@ float2 SampleOffsets[SAMPLE_COUNT]; float SampleWeights[SAMPLE_COUNT]; -float4 PixelShaderF(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 PixelShaderF(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { float4 c = 0; diff --git a/Barotrauma/BarotraumaClient/Shaders/blurshader_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/blurshader_opengl.fx index 14aff9d67..4955b7d58 100644 --- a/Barotrauma/BarotraumaClient/Shaders/blurshader_opengl.fx +++ b/Barotrauma/BarotraumaClient/Shaders/blurshader_opengl.fx @@ -10,7 +10,7 @@ float2 SampleOffsets[SAMPLE_COUNT]; float SampleWeights[SAMPLE_COUNT]; -float4 PixelShaderF(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 PixelShaderF(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { float4 c = 0; diff --git a/Barotrauma/BarotraumaClient/Shaders/damageshader.fx b/Barotrauma/BarotraumaClient/Shaders/damageshader.fx index 8444fd9c6..6f0781293 100644 --- a/Barotrauma/BarotraumaClient/Shaders/damageshader.fx +++ b/Barotrauma/BarotraumaClient/Shaders/damageshader.fx @@ -13,7 +13,7 @@ float aMultiplier; float cCutoff; float cMultiplier; -float4 main(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { float4 c = xTexture.Sample(TextureSampler, texCoord) * inColor; diff --git a/Barotrauma/BarotraumaClient/Shaders/damageshader_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/damageshader_opengl.fx index c50fe7fb9..3a4242a3a 100644 --- a/Barotrauma/BarotraumaClient/Shaders/damageshader_opengl.fx +++ b/Barotrauma/BarotraumaClient/Shaders/damageshader_opengl.fx @@ -13,7 +13,7 @@ float aMultiplier; float cCutoff; float cMultiplier; -float4 main(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { float4 c = tex2D(TextureSampler, texCoord) * inColor; diff --git a/Barotrauma/BarotraumaClient/Shaders/deformshader.fx b/Barotrauma/BarotraumaClient/Shaders/deformshader.fx index dcf54addd..d48e7cded 100644 --- a/Barotrauma/BarotraumaClient/Shaders/deformshader.fx +++ b/Barotrauma/BarotraumaClient/Shaders/deformshader.fx @@ -18,14 +18,14 @@ float4 solidColor; struct VertexShaderInput { - float4 Position : SV_POSITION; + float4 Position : POSITION0; float4 Color : COLOR0; float2 TexCoords: TEXCOORD0; }; struct VertexShaderOutput { - float4 Position : SV_POSITION; + float4 Position : POSITION0; float4 Color : COLOR0; float2 TexCoords: TEXCOORD0; }; @@ -75,6 +75,11 @@ float4 mainPS(VertexShaderOutput input) : COLOR return xTexture.Sample(TextureSampler, input.TexCoords) * input.Color; } +float4 solidVertexColorPS(VertexShaderOutput input) : COLOR +{ + return input.Color * xTexture.Sample(TextureSampler, input.TexCoords).a; +} + float4 solidColorPS(VertexShaderOutput input) : COLOR { return solidColor * xTexture.Sample(TextureSampler, input.TexCoords).a; @@ -96,4 +101,13 @@ technique DeformShaderSolidColor VertexShader = compile vs_4_0_level_9_1 mainVS(); PixelShader = compile ps_4_0_level_9_1 solidColorPS(); } +} + +technique DeformShaderSolidVertexColor +{ + pass Pass1 + { + VertexShader = compile vs_4_0_level_9_1 mainVS(); + PixelShader = compile ps_4_0_level_9_1 solidVertexColorPS(); + } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/Shaders/deformshader_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/deformshader_opengl.fx index c77904ffe..1c151ad17 100644 --- a/Barotrauma/BarotraumaClient/Shaders/deformshader_opengl.fx +++ b/Barotrauma/BarotraumaClient/Shaders/deformshader_opengl.fx @@ -18,14 +18,14 @@ float4 solidColor; struct VertexShaderInput { - float4 Position : SV_POSITION; + float4 Position : POSITION0; float4 Color : COLOR0; float2 TexCoords: TEXCOORD0; }; struct VertexShaderOutput { - float4 Position : SV_POSITION; + float4 Position : POSITION0; float4 Color : COLOR0; float2 TexCoords: TEXCOORD0; }; @@ -75,6 +75,11 @@ float4 mainPS(VertexShaderOutput input) : COLOR return xTexture.Sample(TextureSampler, input.TexCoords) * input.Color; } +float4 solidVertexColorPS(VertexShaderOutput input) : COLOR +{ + return input.Color * xTexture.Sample(TextureSampler, input.TexCoords).a; +} + float4 solidColorPS(VertexShaderOutput input) : COLOR { return solidColor * xTexture.Sample(TextureSampler, input.TexCoords).a; @@ -96,4 +101,13 @@ technique DeformShaderSolidColor VertexShader = compile vs_3_0 mainVS(); PixelShader = compile ps_3_0 solidColorPS(); } +} + +technique DeformShaderSolidVertexColor +{ + pass Pass1 + { + VertexShader = compile vs_3_0 mainVS(); + PixelShader = compile ps_3_0 solidVertexColorPS(); + } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/Shaders/gradientshader.fx b/Barotrauma/BarotraumaClient/Shaders/gradientshader.fx index 26e364323..54dad1b92 100644 --- a/Barotrauma/BarotraumaClient/Shaders/gradientshader.fx +++ b/Barotrauma/BarotraumaClient/Shaders/gradientshader.fx @@ -5,7 +5,7 @@ float4 color2; float midPoint; float fadeDist; -float4 PixelShaderF(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 PixelShaderF(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { float4 t = tex2D(TextureSampler, texCoord); diff --git a/Barotrauma/BarotraumaClient/Shaders/gradientshader_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/gradientshader_opengl.fx index ab209fa73..cbb6bf945 100644 --- a/Barotrauma/BarotraumaClient/Shaders/gradientshader_opengl.fx +++ b/Barotrauma/BarotraumaClient/Shaders/gradientshader_opengl.fx @@ -5,7 +5,7 @@ float4 color2; float midPoint; float fadeDist; -float4 PixelShaderF(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 PixelShaderF(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { float4 t = tex2D(TextureSampler, texCoord); diff --git a/Barotrauma/BarotraumaClient/Shaders/losshader.fx b/Barotrauma/BarotraumaClient/Shaders/losshader.fx index 357e2bad9..d40f91c49 100644 --- a/Barotrauma/BarotraumaClient/Shaders/losshader.fx +++ b/Barotrauma/BarotraumaClient/Shaders/losshader.fx @@ -1,3 +1,24 @@ +struct VertexShaderInput +{ + float4 Position : POSITION0; + float2 TexCoords: TEXCOORD0; +}; + +struct VertexShaderOutput +{ + float4 Position : POSITION0; + float2 TexCoords: TEXCOORD0; +}; + +VertexShaderOutput mainVS(in VertexShaderInput input) +{ + VertexShaderOutput output = (VertexShaderOutput)0; + + output.Position = input.Position; + output.TexCoords = input.TexCoords; + + return output; +} Texture2D xTexture; sampler TextureSampler : register (s0) = sampler_state { Texture = ; }; @@ -5,17 +26,19 @@ sampler TextureSampler : register (s0) = sampler_state { Texture = ; } Texture2D xLosTexture; sampler LosSampler = sampler_state { Texture = ; }; -float4 main(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 xColor; + +float4 mainPS(VertexShaderOutput input) : COLOR0 { - float4 sampleColor = xTexture.Sample(TextureSampler, texCoord); - float4 losColor = xLosTexture.Sample(LosSampler, texCoord); + float4 sampleColor = xTexture.Sample(TextureSampler, input.TexCoords); + float4 losColor = xLosTexture.Sample(LosSampler, input.TexCoords); float obscureAmount = 1.0f - losColor.r; float4 outColor = float4( - sampleColor.r * color.r, - sampleColor.g * color.g, - sampleColor.b * color.b, + sampleColor.r * xColor.r, + sampleColor.g * xColor.g, + sampleColor.b * xColor.b, obscureAmount); return outColor; @@ -25,6 +48,7 @@ technique LosShader { pass Pass1 { - PixelShader = compile ps_4_0_level_9_1 main(); + VertexShader = compile vs_4_0_level_9_1 mainVS(); + PixelShader = compile ps_4_0_level_9_1 mainPS(); } } diff --git a/Barotrauma/BarotraumaClient/Shaders/losshader_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/losshader_opengl.fx index 98805c7ca..23a48e4d2 100644 --- a/Barotrauma/BarotraumaClient/Shaders/losshader_opengl.fx +++ b/Barotrauma/BarotraumaClient/Shaders/losshader_opengl.fx @@ -1,3 +1,24 @@ +struct VertexShaderInput +{ + float4 Position : POSITION0; + float2 TexCoords: TEXCOORD0; +}; + +struct VertexShaderOutput +{ + float4 Position : POSITION0; + float2 TexCoords: TEXCOORD0; +}; + +VertexShaderOutput mainVS(in VertexShaderInput input) +{ + VertexShaderOutput output = (VertexShaderOutput)0; + + output.Position = input.Position; + output.TexCoords = input.TexCoords; + + return output; +} Texture xTexture; sampler TextureSampler : register (s0) = sampler_state { Texture = ; }; @@ -5,17 +26,19 @@ sampler TextureSampler : register (s0) = sampler_state { Texture = ; } Texture xLosTexture; sampler LosSampler = sampler_state { Texture = ; }; -float4 main(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 xColor; + +float4 mainPS(VertexShaderOutput input) : COLOR0 { - float4 sampleColor = tex2D(TextureSampler, texCoord); - float4 losColor = tex2D(LosSampler, texCoord); + float4 sampleColor = tex2D(TextureSampler, input.TexCoords); + float4 losColor = tex2D(LosSampler, input.TexCoords); float obscureAmount = 1.0f - losColor.r; float4 outColor = float4( - sampleColor.r * color.r, - sampleColor.g * color.g, - sampleColor.b * color.b, + sampleColor.r * xColor.r, + sampleColor.g * xColor.g, + sampleColor.b * xColor.b, obscureAmount); return outColor; @@ -25,6 +48,7 @@ technique LosShader { pass Pass1 { - PixelShader = compile ps_2_0 main(); + VertexShader = compile vs_2_0 mainVS(); + PixelShader = compile ps_2_0 mainPS(); } } diff --git a/Barotrauma/BarotraumaClient/Shaders/postprocess.fx b/Barotrauma/BarotraumaClient/Shaders/postprocess.fx index 214ba721d..e618bf0bd 100644 --- a/Barotrauma/BarotraumaClient/Shaders/postprocess.fx +++ b/Barotrauma/BarotraumaClient/Shaders/postprocess.fx @@ -1,3 +1,27 @@ +struct VertexShaderInput +{ + float4 Position : POSITION0; + float2 TexCoords: TEXCOORD0; +}; + +struct VertexShaderOutput +{ + float4 Position : POSITION0; + float2 TexCoords: TEXCOORD0; +}; + +float4x4 MatrixTransform; + +VertexShaderOutput mainVS(in VertexShaderInput input) +{ + VertexShaderOutput output = (VertexShaderOutput)0; + + output.Position = mul(input.Position, MatrixTransform); + output.TexCoords = input.TexCoords; + + return output; +} + Texture2D xTexture; sampler TextureSampler = sampler_state { Texture = ; }; @@ -32,84 +56,84 @@ float2 radialDistortion(float2 coord, float distortion) /*float4 sampleWithChromaticAberration(float2 samplePos) { return float4( - tex2D(TextureSampler, radialDistortion(samplePos, chromaticAberrationStrength.r)).r, - tex2D(TextureSampler, radialDistortion(samplePos, chromaticAberrationStrength.g)).g, - tex2D(TextureSampler, radialDistortion(samplePos, chromaticAberrationStrength.b)).b, + xTexture.Sample(TextureSampler, radialDistortion(samplePos, chromaticAberrationStrength.r)).r, + xTexture.Sample(TextureSampler, radialDistortion(samplePos, chromaticAberrationStrength.g)).g, + xTexture.Sample(TextureSampler, radialDistortion(samplePos, chromaticAberrationStrength.b)).b, 1); }*/ float3 sampleChannelsSeparately(float2 samplePosR, float2 samplePosG, float2 samplePosB) { return float3( - tex2D(TextureSampler, samplePosR).r, - tex2D(TextureSampler, samplePosG).g, - tex2D(TextureSampler, samplePosB).b); + xTexture.Sample(TextureSampler, samplePosR).r, + xTexture.Sample(TextureSampler, samplePosG).g, + xTexture.Sample(TextureSampler, samplePosB).b); } float3 sampleWithChromaticAberration(float2 samplePos) { return float3( - tex2D(TextureSampler, radialDistortion(samplePos, chromaticAberrationStrength.r)).r, - tex2D(TextureSampler, radialDistortion(samplePos, chromaticAberrationStrength.g)).g, - tex2D(TextureSampler, radialDistortion(samplePos, chromaticAberrationStrength.b)).b); + xTexture.Sample(TextureSampler, radialDistortion(samplePos, chromaticAberrationStrength.r)).r, + xTexture.Sample(TextureSampler, radialDistortion(samplePos, chromaticAberrationStrength.g)).g, + xTexture.Sample(TextureSampler, radialDistortion(samplePos, chromaticAberrationStrength.b)).b); } -float4 blur(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 blur(VertexShaderOutput input) : COLOR0 { float4 sample; - sample = tex2D(TextureSampler, float2(texCoord.x + blurDistance, texCoord.y + blurDistance)); - sample += tex2D(TextureSampler, float2(texCoord.x - blurDistance, texCoord.y - blurDistance)); - sample += tex2D(TextureSampler, float2(texCoord.x + blurDistance, texCoord.y - blurDistance)); - sample += tex2D(TextureSampler, float2(texCoord.x - blurDistance, texCoord.y + blurDistance)); + sample = xTexture.Sample(TextureSampler, float2(input.TexCoords.x + blurDistance, input.TexCoords.y + blurDistance)); + sample += xTexture.Sample(TextureSampler, float2(input.TexCoords.x - blurDistance, input.TexCoords.y - blurDistance)); + sample += xTexture.Sample(TextureSampler, float2(input.TexCoords.x + blurDistance, input.TexCoords.y - blurDistance)); + sample += xTexture.Sample(TextureSampler, float2(input.TexCoords.x - blurDistance, input.TexCoords.y + blurDistance)); sample = sample * 0.25f; return sample; } -float4 distort(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 distort(VertexShaderOutput input) : COLOR0 { - float4 bumpColor = tex2D(DistortSampler, texCoord + distortUvOffset); - bumpColor = (bumpColor + tex2D(DistortSampler, texCoord - distortUvOffset * 2.0f)) * 0.5f; + float4 bumpColor = xDistortTexture.Sample(DistortSampler, input.TexCoords + distortUvOffset); + bumpColor = (bumpColor + xDistortTexture.Sample(DistortSampler, input.TexCoords - distortUvOffset * 2.0f)) * 0.5f; - float2 samplePos = texCoord; + float2 samplePos = input.TexCoords; samplePos.x += (bumpColor.r - 0.5f) * distortScale.x; samplePos.y += (bumpColor.g - 0.5f) * distortScale.y; - return tex2D(TextureSampler, samplePos); + return xTexture.Sample(TextureSampler, samplePos); } -float4 blurDistort(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 blurDistort(VertexShaderOutput input) : COLOR0 { - float4 bumpColor = tex2D(DistortSampler, texCoord + distortUvOffset); - bumpColor = (bumpColor + tex2D(DistortSampler, texCoord - distortUvOffset * 2.0f)) * 0.5f; + float4 bumpColor = xDistortTexture.Sample(DistortSampler, input.TexCoords + distortUvOffset); + bumpColor = (bumpColor + xDistortTexture.Sample(DistortSampler, input.TexCoords - distortUvOffset * 2.0f)) * 0.5f; - float2 samplePos = texCoord; + float2 samplePos = input.TexCoords; samplePos.x += (bumpColor.r - 0.5f) * distortScale.x; samplePos.y += (bumpColor.g - 0.5f) * distortScale.y; float4 sample; - sample = tex2D(TextureSampler, float2(samplePos.x + blurDistance, samplePos.y + blurDistance)); - sample += tex2D(TextureSampler, float2(samplePos.x - blurDistance, samplePos.y - blurDistance)); - sample += tex2D(TextureSampler, float2(samplePos.x + blurDistance, samplePos.y - blurDistance)); - sample += tex2D(TextureSampler, float2(samplePos.x - blurDistance, samplePos.y + blurDistance)); + sample = xTexture.Sample(TextureSampler, float2(samplePos.x + blurDistance, samplePos.y + blurDistance)); + sample += xTexture.Sample(TextureSampler, float2(samplePos.x - blurDistance, samplePos.y - blurDistance)); + sample += xTexture.Sample(TextureSampler, float2(samplePos.x + blurDistance, samplePos.y - blurDistance)); + sample += xTexture.Sample(TextureSampler, float2(samplePos.x - blurDistance, samplePos.y + blurDistance)); sample = sample * 0.25f; return sample; } -float4 chromaticAberration(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 chromaticAberration(VertexShaderOutput input) : COLOR0 { - return float4(sampleWithChromaticAberration(texCoord), 1); + return float4(sampleWithChromaticAberration(input.TexCoords), 1); } -float4 chromaticAberrationDistort(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 chromaticAberrationDistort(VertexShaderOutput input) : COLOR0 { - float4 bumpColor = tex2D(DistortSampler, texCoord + distortUvOffset); - bumpColor = (bumpColor + tex2D(DistortSampler, texCoord - distortUvOffset * 2.0f)) * 0.5f; + float4 bumpColor = xDistortTexture.Sample(DistortSampler, input.TexCoords + distortUvOffset); + bumpColor = (bumpColor + xDistortTexture.Sample(DistortSampler, input.TexCoords - distortUvOffset * 2.0f)) * 0.5f; - float2 samplePos = texCoord; + float2 samplePos = input.TexCoords; samplePos.x += (bumpColor.r - 0.5f) * distortScale.x; samplePos.y += (bumpColor.g - 0.5f) * distortScale.y; @@ -117,11 +141,11 @@ float4 chromaticAberrationDistort(float4 position : SV_Position, float4 color : return float4(sampleWithChromaticAberration(samplePos), 1); } -float4 blurChromaticAberration(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 blurChromaticAberration(VertexShaderOutput input) : COLOR0 { - float2 samplePosR = radialDistortion(texCoord, chromaticAberrationStrength.r); - float2 samplePosG = radialDistortion(texCoord, chromaticAberrationStrength.g); - float2 samplePosB = radialDistortion(texCoord, chromaticAberrationStrength.b); + float2 samplePosR = radialDistortion(input.TexCoords, chromaticAberrationStrength.r); + float2 samplePosG = radialDistortion(input.TexCoords, chromaticAberrationStrength.g); + float2 samplePosB = radialDistortion(input.TexCoords, chromaticAberrationStrength.b); float2 blurTopLeft = -blurDistance; float2 blurTopRight = float2(blurDistance, -blurDistance); @@ -139,12 +163,12 @@ float4 blurChromaticAberration(float4 position : SV_Position, float4 color : COL return float4(sample, 1); } -float4 blurChromaticAberrationDistort(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 blurChromaticAberrationDistort(VertexShaderOutput input) : COLOR0 { - float4 bumpColor = tex2D(DistortSampler, texCoord + distortUvOffset); - bumpColor = (bumpColor + tex2D(DistortSampler, texCoord - distortUvOffset * 2.0f)) * 0.5f; + float4 bumpColor = xDistortTexture.Sample(DistortSampler, input.TexCoords + distortUvOffset); + bumpColor = (bumpColor + xDistortTexture.Sample(DistortSampler, input.TexCoords - distortUvOffset * 2.0f)) * 0.5f; - float2 samplePos = texCoord; + float2 samplePos = input.TexCoords; samplePos.x += (bumpColor.r - 0.5f) * distortScale.x; samplePos.y += (bumpColor.g - 0.5f) * distortScale.y; @@ -173,6 +197,7 @@ technique Distort { pass Pass1 { + VertexShader = compile vs_4_0_level_9_1 mainVS(); PixelShader = compile ps_4_0_level_9_1 distort(); } } @@ -181,6 +206,7 @@ technique Blur { pass Pass1 { + VertexShader = compile vs_4_0_level_9_1 mainVS(); PixelShader = compile ps_4_0_level_9_1 blur(); } } @@ -189,6 +215,7 @@ technique BlurDistort { pass Pass1 { + VertexShader = compile vs_4_0_level_9_1 mainVS(); PixelShader = compile ps_4_0_level_9_1 blurDistort(); } } @@ -197,6 +224,7 @@ technique BlurChromaticAberration { pass Pass1 { + VertexShader = compile vs_4_0_level_9_1 mainVS(); PixelShader = compile ps_4_0_level_9_1 blurChromaticAberration(); } } @@ -206,6 +234,7 @@ technique ChromaticAberration { pass Pass1 { + VertexShader = compile vs_4_0_level_9_1 mainVS(); PixelShader = compile ps_4_0_level_9_1 chromaticAberration(); } } @@ -214,6 +243,7 @@ technique ChromaticAberrationDistort { pass Pass1 { + VertexShader = compile vs_4_0_level_9_1 mainVS(); PixelShader = compile ps_4_0_level_9_1 chromaticAberrationDistort(); } } @@ -222,6 +252,7 @@ technique BlurChromaticAberrationDistort { pass Pass1 { + VertexShader = compile vs_4_0_level_9_1 mainVS(); PixelShader = compile ps_4_0_level_9_1 blurChromaticAberrationDistort(); } } diff --git a/Barotrauma/BarotraumaClient/Shaders/postprocess_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/postprocess_opengl.fx index 8941d42ef..063b2f107 100644 --- a/Barotrauma/BarotraumaClient/Shaders/postprocess_opengl.fx +++ b/Barotrauma/BarotraumaClient/Shaders/postprocess_opengl.fx @@ -1,7 +1,31 @@ -Texture2D xTexture; +struct VertexShaderInput +{ + float4 Position : POSITION0; + float2 TexCoords: TEXCOORD0; +}; + +struct VertexShaderOutput +{ + float4 Position : POSITION0; + float2 TexCoords: TEXCOORD0; +}; + +float4x4 MatrixTransform; + +VertexShaderOutput mainVS(in VertexShaderInput input) +{ + VertexShaderOutput output = (VertexShaderOutput)0; + + output.Position = mul(input.Position, MatrixTransform); + output.TexCoords = input.TexCoords; + + return output; +} + +Texture xTexture; sampler TextureSampler = sampler_state { Texture = ; }; -Texture2D xDistortTexture; +Texture xDistortTexture; sampler DistortSampler = sampler_state { @@ -53,24 +77,24 @@ float3 sampleWithChromaticAberration(float2 samplePos) tex2D(TextureSampler, radialDistortion(samplePos, chromaticAberrationStrength.b)).b); } -float4 blur(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 blur(VertexShaderOutput input) : COLOR0 { float4 sample; - sample = tex2D(TextureSampler, float2(texCoord.x + blurDistance, texCoord.y + blurDistance)); - sample += tex2D(TextureSampler, float2(texCoord.x - blurDistance, texCoord.y - blurDistance)); - sample += tex2D(TextureSampler, float2(texCoord.x + blurDistance, texCoord.y - blurDistance)); - sample += tex2D(TextureSampler, float2(texCoord.x - blurDistance, texCoord.y + blurDistance)); + sample = tex2D(TextureSampler, float2(input.TexCoords.x + blurDistance, input.TexCoords.y + blurDistance)); + sample += tex2D(TextureSampler, float2(input.TexCoords.x - blurDistance, input.TexCoords.y - blurDistance)); + sample += tex2D(TextureSampler, float2(input.TexCoords.x + blurDistance, input.TexCoords.y - blurDistance)); + sample += tex2D(TextureSampler, float2(input.TexCoords.x - blurDistance, input.TexCoords.y + blurDistance)); sample = sample * 0.25f; return sample; } -float4 distort(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 distort(VertexShaderOutput input) : COLOR0 { - float4 bumpColor = tex2D(DistortSampler, texCoord + distortUvOffset); - bumpColor = (bumpColor + tex2D(DistortSampler, texCoord - distortUvOffset * 2.0f)) * 0.5f; + float4 bumpColor = tex2D(DistortSampler, input.TexCoords + distortUvOffset); + bumpColor = (bumpColor + tex2D(DistortSampler, input.TexCoords - distortUvOffset * 2.0f)) * 0.5f; - float2 samplePos = texCoord; + float2 samplePos = input.TexCoords; samplePos.x += (bumpColor.r - 0.5f) * distortScale.x; samplePos.y += (bumpColor.g - 0.5f) * distortScale.y; @@ -78,12 +102,12 @@ float4 distort(float4 position : SV_Position, float4 color : COLOR0, float2 texC return tex2D(TextureSampler, samplePos); } -float4 blurDistort(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 blurDistort(VertexShaderOutput input) : COLOR0 { - float4 bumpColor = tex2D(DistortSampler, texCoord + distortUvOffset); - bumpColor = (bumpColor + tex2D(DistortSampler, texCoord - distortUvOffset * 2.0f)) * 0.5f; + float4 bumpColor = tex2D(DistortSampler, input.TexCoords + distortUvOffset); + bumpColor = (bumpColor + tex2D(DistortSampler, input.TexCoords - distortUvOffset * 2.0f)) * 0.5f; - float2 samplePos = texCoord; + float2 samplePos = input.TexCoords; samplePos.x += (bumpColor.r - 0.5f) * distortScale.x; samplePos.y += (bumpColor.g - 0.5f) * distortScale.y; @@ -99,17 +123,17 @@ float4 blurDistort(float4 position : SV_Position, float4 color : COLOR0, float2 return sample; } -float4 chromaticAberration(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 chromaticAberration(VertexShaderOutput input) : COLOR0 { - return float4(sampleWithChromaticAberration(texCoord), 1); + return float4(sampleWithChromaticAberration(input.TexCoords), 1); } -float4 chromaticAberrationDistort(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 chromaticAberrationDistort(VertexShaderOutput input) : COLOR0 { - float4 bumpColor = tex2D(DistortSampler, texCoord + distortUvOffset); - bumpColor = (bumpColor + tex2D(DistortSampler, texCoord - distortUvOffset * 2.0f)) * 0.5f; + float4 bumpColor = tex2D(DistortSampler, input.TexCoords + distortUvOffset); + bumpColor = (bumpColor + tex2D(DistortSampler, input.TexCoords - distortUvOffset * 2.0f)) * 0.5f; - float2 samplePos = texCoord; + float2 samplePos = input.TexCoords; samplePos.x += (bumpColor.r - 0.5f) * distortScale.x; samplePos.y += (bumpColor.g - 0.5f) * distortScale.y; @@ -117,11 +141,11 @@ float4 chromaticAberrationDistort(float4 position : SV_Position, float4 color : return float4(sampleWithChromaticAberration(samplePos), 1); } -float4 blurChromaticAberration(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 blurChromaticAberration(VertexShaderOutput input) : COLOR0 { - float2 samplePosR = radialDistortion(texCoord, chromaticAberrationStrength.r); - float2 samplePosG = radialDistortion(texCoord, chromaticAberrationStrength.g); - float2 samplePosB = radialDistortion(texCoord, chromaticAberrationStrength.b); + float2 samplePosR = radialDistortion(input.TexCoords, chromaticAberrationStrength.r); + float2 samplePosG = radialDistortion(input.TexCoords, chromaticAberrationStrength.g); + float2 samplePosB = radialDistortion(input.TexCoords, chromaticAberrationStrength.b); float2 blurTopLeft = -blurDistance; float2 blurTopRight = float2(blurDistance, -blurDistance); @@ -139,12 +163,12 @@ float4 blurChromaticAberration(float4 position : SV_Position, float4 color : COL return float4(sample, 1); } -float4 blurChromaticAberrationDistort(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 blurChromaticAberrationDistort(VertexShaderOutput input) : COLOR0 { - float4 bumpColor = tex2D(DistortSampler, texCoord + distortUvOffset); - bumpColor = (bumpColor + tex2D(DistortSampler, texCoord - distortUvOffset * 2.0f)) * 0.5f; + float4 bumpColor = tex2D(DistortSampler, input.TexCoords + distortUvOffset); + bumpColor = (bumpColor + tex2D(DistortSampler, input.TexCoords - distortUvOffset * 2.0f)) * 0.5f; - float2 samplePos = texCoord; + float2 samplePos = input.TexCoords; samplePos.x += (bumpColor.r - 0.5f) * distortScale.x; samplePos.y += (bumpColor.g - 0.5f) * distortScale.y; @@ -173,6 +197,7 @@ technique Distort { pass Pass1 { + VertexShader = compile vs_3_0 mainVS(); PixelShader = compile ps_3_0 distort(); } } @@ -181,6 +206,7 @@ technique Blur { pass Pass1 { + VertexShader = compile vs_3_0 mainVS(); PixelShader = compile ps_3_0 blur(); } } @@ -189,6 +215,7 @@ technique BlurDistort { pass Pass1 { + VertexShader = compile vs_3_0 mainVS(); PixelShader = compile ps_3_0 blurDistort(); } } @@ -197,6 +224,7 @@ technique BlurChromaticAberration { pass Pass1 { + VertexShader = compile vs_3_0 mainVS(); PixelShader = compile ps_3_0 blurChromaticAberration(); } } @@ -206,6 +234,7 @@ technique ChromaticAberration { pass Pass1 { + VertexShader = compile vs_3_0 mainVS(); PixelShader = compile ps_3_0 chromaticAberration(); } } @@ -214,6 +243,7 @@ technique ChromaticAberrationDistort { pass Pass1 { + VertexShader = compile vs_3_0 mainVS(); PixelShader = compile ps_3_0 chromaticAberrationDistort(); } } @@ -222,6 +252,7 @@ technique BlurChromaticAberrationDistort { pass Pass1 { + VertexShader = compile vs_3_0 mainVS(); PixelShader = compile ps_3_0 blurChromaticAberrationDistort(); } } diff --git a/Barotrauma/BarotraumaClient/Shaders/solidcolor.fx b/Barotrauma/BarotraumaClient/Shaders/solidcolor.fx index 846f3d2fe..464851d70 100644 --- a/Barotrauma/BarotraumaClient/Shaders/solidcolor.fx +++ b/Barotrauma/BarotraumaClient/Shaders/solidcolor.fx @@ -4,13 +4,19 @@ sampler TextureSampler = sampler_state { Texture = ; }; float blurDistance; float4 color; -float4 solidColor(float4 position : SV_Position, float4 clr : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 solidColor(float4 position : POSITION0, float4 clr : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { float a = tex2D(TextureSampler, texCoord).a; return color * a; } -float4 solidColorBlur(float4 position : SV_Position, float4 clr : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 solidVertexColor(float4 position : POSITION0, float4 clr : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +{ + float a = tex2D(TextureSampler, texCoord).a; + return clr * a; +} + +float4 solidColorBlur(float4 position : POSITION0, float4 clr : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { float sample; sample = tex2D(TextureSampler, float2(texCoord.x + blurDistance, texCoord.y + blurDistance)).a; @@ -22,13 +28,20 @@ float4 solidColorBlur(float4 position : SV_Position, float4 clr : COLOR0, float2 return color * sample; } -technique SolidColor +technique SolidColor { pass Pass1 { PixelShader = compile ps_4_0_level_9_1 solidColor(); } } +technique SolidVertexColor +{ + pass Pass1 + { + PixelShader = compile ps_4_0_level_9_1 solidVertexColor(); + } +} technique SolidColorBlur { pass Pass1 diff --git a/Barotrauma/BarotraumaClient/Shaders/solidcolor_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/solidcolor_opengl.fx index 2c82b0bf3..40ad68196 100644 --- a/Barotrauma/BarotraumaClient/Shaders/solidcolor_opengl.fx +++ b/Barotrauma/BarotraumaClient/Shaders/solidcolor_opengl.fx @@ -1,16 +1,22 @@ -Texture xTexture; +Texture2D xTexture; sampler TextureSampler = sampler_state { Texture = ; }; float blurDistance; float4 color; -float4 solidColor(float4 position : SV_Position, float4 clr : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 solidColor(float4 position : POSITION0, float4 clr : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { float a = tex2D(TextureSampler, texCoord).a; return color * a; } -float4 solidColorBlur(float4 position : SV_Position, float4 clr : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 solidVertexColor(float4 position : POSITION0, float4 clr : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +{ + float a = tex2D(TextureSampler, texCoord).a; + return clr * a; +} + +float4 solidColorBlur(float4 position : POSITION0, float4 clr : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { float sample; sample = tex2D(TextureSampler, float2(texCoord.x + blurDistance, texCoord.y + blurDistance)).a; @@ -22,13 +28,20 @@ float4 solidColorBlur(float4 position : SV_Position, float4 clr : COLOR0, float2 return color * sample; } -technique SolidColor +technique SolidColor { pass Pass1 { PixelShader = compile ps_3_0 solidColor(); } } +technique SolidVertexColor +{ + pass Pass1 + { + PixelShader = compile ps_3_0 solidVertexColor(); + } +} technique SolidColorBlur { pass Pass1 diff --git a/Barotrauma/BarotraumaClient/Shaders/watershader.fx b/Barotrauma/BarotraumaClient/Shaders/watershader.fx index 331e860ec..a8f053ed2 100644 --- a/Barotrauma/BarotraumaClient/Shaders/watershader.fx +++ b/Barotrauma/BarotraumaClient/Shaders/watershader.fx @@ -20,14 +20,14 @@ float4x4 xUvTransform; struct VertexShaderInput { - float4 Position : SV_POSITION; + float4 Position : POSITION0; float4 Color : COLOR0; float2 TexCoords: TEXCOORD0; // added }; struct VertexShaderOutput { - float4 Position : SV_POSITION; + float4 Position : POSITION0; float4 Color : COLOR0; float2 TexCoords: TEXCOORD0; // added }; @@ -95,7 +95,7 @@ float4 mainPSBlurred(VertexShaderOutput input) : COLOR return sample; } -float4 mainPostProcess(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 mainPostProcess(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { float4 bumpColor = tex2D(WaterBumpSampler, texCoord + xUvOffset + xBumpPos); bumpColor = (bumpColor + tex2D(WaterBumpSampler, texCoord - xUvOffset * 2.0f + xBumpPos)) * 0.5f; diff --git a/Barotrauma/BarotraumaClient/Shaders/watershader_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/watershader_opengl.fx index ddef6b01e..842f5b201 100644 --- a/Barotrauma/BarotraumaClient/Shaders/watershader_opengl.fx +++ b/Barotrauma/BarotraumaClient/Shaders/watershader_opengl.fx @@ -20,14 +20,14 @@ float4x4 xUvTransform; struct VertexShaderInput { - float4 Position : SV_POSITION; + float4 Position : POSITION0; float4 Color : COLOR0; float2 TexCoords: TEXCOORD0; // added }; struct VertexShaderOutput { - float4 Position : SV_POSITION; + float4 Position : POSITION0; float4 Color : COLOR0; float2 TexCoords: TEXCOORD0; // added }; @@ -102,7 +102,7 @@ float4 mainPSBlurred(VertexShaderOutput input) : COLOR return sample; } -float4 mainPostProcess(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 mainPostProcess(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { float4 bumpColor = tex2D(WaterBumpSampler, texCoord + xUvOffset + xBumpPos); bumpColor = (bumpColor + tex2D(WaterBumpSampler, texCoord - xUvOffset * 2.0f + xBumpPos)) * 0.5f; diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 4642e0645..74b2aeb89 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.9.9.0 + 0.9.1000.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index e6e084590..b2396bd0f 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.9.9.0 + 0.9.1000.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index eda82c727..bf962cfdb 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.9.9.0 + 0.9.1000.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs index 618535b40..fed973a06 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs @@ -9,9 +9,9 @@ namespace Barotrauma partial void InitProjSpecific(XElement mainElement) { } - partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult) + partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult, float stun) { - GameMain.Server.KarmaManager.OnCharacterHealthChanged(this, attacker, attackResult.Damage, attackResult.Afflictions); + GameMain.Server.KarmaManager.OnCharacterHealthChanged(this, attacker, attackResult.Damage, stun, attackResult.Afflictions); } partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool log) @@ -20,11 +20,11 @@ namespace Barotrauma { if (causeOfDeath == CauseOfDeathType.Affliction) { - GameServer.Log(LogName + " has died (Cause of death: " + causeOfDeathAffliction.Prefab.Name + ")", ServerLog.MessageType.Attack); + GameServer.Log(GameServer.CharacterLogName(this) + " has died (Cause of death: " + causeOfDeathAffliction.Prefab.Name + ")", ServerLog.MessageType.Attack); } else { - GameServer.Log(LogName + " has died (Cause of death: " + causeOfDeath + ")", ServerLog.MessageType.Attack); + GameServer.Log(GameServer.CharacterLogName(this) + " has died (Cause of death: " + causeOfDeath + ")", ServerLog.MessageType.Attack); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index 7e0bdcd8b..390b4cb98 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -264,21 +264,21 @@ namespace Barotrauma switch ((NetEntityEvent.Type)extraData[0]) { case NetEntityEvent.Type.InventoryState: - msg.WriteRangedInteger(0, 0, 3); + msg.WriteRangedInteger(0, 0, 4); msg.Write(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0); Inventory.ServerWrite(msg, c); break; case NetEntityEvent.Type.Control: - msg.WriteRangedInteger(1, 0, 3); + msg.WriteRangedInteger(1, 0, 4); Client owner = (Client)extraData[1]; msg.Write(owner != null && owner.Character == this && GameMain.Server.ConnectedClients.Contains(owner) ? owner.ID : (byte)0); break; case NetEntityEvent.Type.Status: - msg.WriteRangedInteger(2, 0, 3); + msg.WriteRangedInteger(2, 0, 4); WriteStatus(msg); break; case NetEntityEvent.Type.UpdateSkills: - msg.WriteRangedInteger(3, 0, 3); + msg.WriteRangedInteger(3, 0, 4); if (Info?.Job == null) { msg.Write((byte)0); @@ -293,6 +293,15 @@ namespace Barotrauma } } break; + case NetEntityEvent.Type.ExecuteAttack: + Limb attackLimb = extraData[1] as Limb; + UInt16 targetEntityID = (UInt16)extraData[2]; + int targetLimbIndex = extraData.Length > 3 ? (int)extraData[3] : 0; + msg.WriteRangedInteger(4, 0, 4); + msg.Write((byte)(Removed ? 255 : Array.IndexOf(AnimController.Limbs, attackLimb))); + msg.Write(targetEntityID); + msg.Write((byte)targetLimbIndex); + break; default: DebugConsole.ThrowError("Invalid NetworkEvent type for entity " + ToString() + " (" + (NetEntityEvent.Type)extraData[0] + ")"); break; @@ -407,6 +416,7 @@ namespace Barotrauma } } + private List severedJointIndices = new List(); private void WriteStatus(IWriteMessage msg) { msg.Write(IsDead); @@ -417,33 +427,32 @@ namespace Barotrauma { msg.Write(CauseOfDeath.Affliction.Identifier); } - - if (AnimController?.LimbJoints == null) - { - //0 limbs severed - msg.Write((byte)0); - } - else - { - List severedJointIndices = new List(); - for (int i = 0; i < AnimController.LimbJoints.Length; i++) - { - if (AnimController.LimbJoints[i] != null && AnimController.LimbJoints[i].IsSevered) - { - severedJointIndices.Add(i); - } - } - msg.Write((byte)severedJointIndices.Count); - foreach (int jointIndex in severedJointIndices) - { - msg.Write((byte)jointIndex); - } - } } else { CharacterHealth.ServerWrite(msg); } + if (AnimController?.LimbJoints == null) + { + //0 limbs severed + msg.Write((byte)0); + } + else + { + severedJointIndices.Clear(); + for (int i = 0; i < AnimController.LimbJoints.Length; i++) + { + if (AnimController.LimbJoints[i] != null && AnimController.LimbJoints[i].IsSevered) + { + severedJointIndices.Add(i); + } + } + msg.Write((byte)severedJointIndices.Count); + foreach (int jointIndex in severedJointIndices) + { + msg.Write((byte)jointIndex); + } + } } public void WriteSpawnData(IWriteMessage msg, UInt16 entityId, bool restrictMessageSize) diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index e08792a13..c16cff5b1 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -7,7 +7,7 @@ using System.ComponentModel; using FarseerPhysics; using Barotrauma.Items.Components; using System.Threading; -using System.IO; +using Barotrauma.IO; using System.Text; using System.Diagnostics; @@ -82,10 +82,6 @@ namespace Barotrauma { if (queuedMessages.Count > 0) { - int inputLines = Math.Max((int)Math.Ceiling(input.Length / (float)Console.WindowWidth), 1); - Console.CursorLeft = 0; - Console.Write(new string(' ', consoleWidth)); - Console.CursorTop = Math.Max(Console.CursorTop - inputLines, 0); Console.CursorLeft = 0; while (queuedMessages.Count > 0) { @@ -192,32 +188,64 @@ namespace Barotrauma sw.Stop(); } + private static void WriteAndResetLine(string txt) + { + int consoleWidth = Console.BufferWidth; + int linesWritten = 0; + while (true) + { + if (txt.Length > consoleWidth) + { + linesWritten++; + Console.Write(txt.Substring(0, consoleWidth)); + txt = txt.Substring(consoleWidth); + } + else + { + Console.Write(txt); + if (txt.Length == consoleWidth) + { + Console.Write(' '); Console.CursorLeft--; + linesWritten++; + } + break; + } + } + Console.CursorTop -= linesWritten; + } + private static void RewriteInputToCommandLine(string input) { if (Console.WindowWidth == 0 || Console.WindowHeight == 0) { return; } - int consoleWidth = Math.Max(Console.WindowWidth, 5); - int inputLines = Math.Max((int)Math.Ceiling(input.Length / (float)consoleWidth), 1); - int cursorLine = Math.Max((int)Math.Ceiling((input.Length + 1) / (float)consoleWidth), 1); + int consoleWidth = Math.Max(Console.BufferWidth, 5); + //int inputLines = Math.Max((int)Math.Ceiling(input.Length / (float)consoleWidth), 1); + //int cursorLine = Math.Max((int)Math.Ceiling((input.Length + 1) / (float)consoleWidth), 1); try { - Console.WriteLine(""); Console.CursorTop -= inputLines; - + string tmpInput = input; + while (tmpInput.Length >= consoleWidth) + { + tmpInput = tmpInput.Substring(consoleWidth); + } string ln = input.Length > 0 ? AutoComplete(input, 0) : ""; + while (ln.Length >= consoleWidth) + { + ln = ln.Substring(consoleWidth); + } ln += new string(' ', consoleWidth - (ln.Length % consoleWidth)); Console.ForegroundColor = ConsoleColor.DarkGray; Console.CursorLeft = 0; - Console.Write(ln); + WriteAndResetLine(ln); Console.ForegroundColor = ConsoleColor.White; Console.CursorLeft = 0; - Console.CursorTop -= cursorLine; - Console.Write(input); + WriteAndResetLine(tmpInput); Console.CursorLeft = input.Length % consoleWidth; } catch (Exception e) { - string errorMsg = "Failed to write input to command line (window width: " + Console.WindowWidth + ", window height: " + Console.WindowHeight + ", inputLines:" + inputLines + ")\n" + string errorMsg = "Failed to write input to command line (window width: " + Console.WindowWidth + ", window height: " + Console.WindowHeight + ")\n" + e.Message + "\n" + e.StackTrace; GameAnalyticsManager.AddErrorEventOnce("DebugConsole.RewriteInputToCommandLine", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); } @@ -948,7 +976,7 @@ namespace Barotrauma NewMessage("***************", Color.Cyan); foreach (Client c in GameMain.Server.ConnectedClients) { - NewMessage("- " + c.ID.ToString() + ": " + c.Name + (c.Character != null ? " playing " + c.Character.LogName : "") + ", " + c.Connection.EndPointString, Color.Cyan); + NewMessage("- " + c.ID.ToString() + ": " + c.Name + (c.Character != null ? " playing " + c.Character.LogName : "") + ", " + c.Connection.EndPointString + $", ping {c.Ping} ms", Color.Cyan); } NewMessage("***************", Color.Cyan); })); @@ -957,7 +985,7 @@ namespace Barotrauma GameMain.Server.SendConsoleMessage("***************", client); foreach (Client c in GameMain.Server.ConnectedClients) { - GameMain.Server.SendConsoleMessage("- " + c.ID.ToString() + ": " + c.Name + ", " + c.Connection.EndPointString, client); + GameMain.Server.SendConsoleMessage("- " + c.ID.ToString() + ": " + c.Name + ", " + c.Connection.EndPointString + $", ping {c.Ping} ms", client); } GameMain.Server.SendConsoleMessage("***************", client); }); @@ -1597,13 +1625,14 @@ namespace Barotrauma (Client client, Vector2 cursorWorldPos, string[] args) => { Vector2 explosionPos = cursorWorldPos; - float range = 500, force = 10, damage = 50, structureDamage = 10, empStrength = 0.0f; ; + float range = 500, force = 10, damage = 50, structureDamage = 10, itemDamage = 100, empStrength = 0.0f; ; if (args.Length > 0) float.TryParse(args[0], out range); if (args.Length > 1) float.TryParse(args[1], out force); if (args.Length > 2) float.TryParse(args[2], out damage); if (args.Length > 3) float.TryParse(args[3], out structureDamage); - if (args.Length > 4) float.TryParse(args[4], out empStrength); - new Explosion(range, force, damage, structureDamage, empStrength).Explode(explosionPos, null); + if (args.Length > 4) float.TryParse(args[4], out itemDamage); + if (args.Length > 5) float.TryParse(args[5], out empStrength); + new Explosion(range, force, damage, structureDamage, itemDamage, empStrength).Explode(explosionPos, null); } ); @@ -1912,6 +1941,27 @@ namespace Barotrauma } ); + AssignOnClientRequestExecute( + "money", + (Client senderClient, Vector2 cursorWorldPos, string[] args) => + { + if (args.Length == 0) { return; } + if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign)) + { + GameMain.Server.SendConsoleMessage("No campaign active!", senderClient); + return; + } + if (int.TryParse(args[0], out int money)) + { + campaign.Money += money; + campaign.LastUpdateID++; + } + else + { + GameMain.Server.SendConsoleMessage($"\"{args[0]}\" is not a valid numeric value.", senderClient); + } + } + ); AssignOnClientRequestExecute( "campaigndestination|setcampaigndestination", (Client senderClient, Vector2 cursorWorldPos, string[] args) => @@ -1987,7 +2037,7 @@ namespace Barotrauma if (!client.HasPermission(ClientPermissions.ConsoleCommands) && client.Connection != GameMain.Server.OwnerConnection) { GameMain.Server.SendConsoleMessage("You are not permitted to use console commands!", client); - GameServer.Log(client.Name + " attempted to execute the console command \"" + command + "\" without a permission to use console commands.", ServerLog.MessageType.ConsoleUsage); + GameServer.Log(GameServer.ClientLogName(client) + " attempted to execute the console command \"" + command + "\" without a permission to use console commands.", ServerLog.MessageType.ConsoleUsage); return; } @@ -1996,7 +2046,7 @@ namespace Barotrauma if (matchingCommand != null && !client.PermittedConsoleCommands.Contains(matchingCommand) && client.Connection != GameMain.Server.OwnerConnection) { GameMain.Server.SendConsoleMessage("You are not permitted to use the command\"" + matchingCommand.names[0] + "\"!", client); - GameServer.Log(client.Name + " attempted to execute the console command \"" + command + "\" without a permission to use the command.", ServerLog.MessageType.ConsoleUsage); + GameServer.Log(GameServer.ClientLogName(client) + " attempted to execute the console command \"" + command + "\" without a permission to use the command.", ServerLog.MessageType.ConsoleUsage); return; } else if (matchingCommand == null) @@ -2008,18 +2058,18 @@ namespace Barotrauma if (!MathUtils.IsValid(cursorWorldPos)) { GameMain.Server.SendConsoleMessage("Could not execute command \"" + command + "\" - invalid cursor position.", client); - NewMessage(client.Name + " attempted to execute the console command \"" + command + "\" with invalid cursor position.", Color.White); + NewMessage(GameServer.ClientLogName(client) + " attempted to execute the console command \"" + command + "\" with invalid cursor position.", Color.White); return; } try { matchingCommand.ServerExecuteOnClientRequest(client, cursorWorldPos, splitCommand.Skip(1).ToArray()); - GameServer.Log("Console command \"" + command + "\" executed by " + client.Name + ".", ServerLog.MessageType.ConsoleUsage); + GameServer.Log("Console command \"" + command + "\" executed by " + GameServer.ClientLogName(client) + ".", ServerLog.MessageType.ConsoleUsage); } catch (Exception e) { - ThrowError("Executing the command \"" + matchingCommand.names[0] + "\" by request from \"" + client.Name + "\" failed.", e); + ThrowError("Executing the command \"" + matchingCommand.names[0] + "\" by request from \"" + GameServer.ClientLogName(client) + "\" failed.", e); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CargoMission.cs index 7c3cd5b32..9ee80f0f8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CargoMission.cs @@ -9,7 +9,7 @@ namespace Barotrauma msg.Write((ushort)items.Count); foreach (Item item in items) { - item.WriteSpawnData(msg, item.ID); + item.WriteSpawnData(msg, item.ID, item.ParentInventory?.Owner?.ID ?? 0); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs index 3ebe4b95b..1ceed8427 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs @@ -1,9 +1,14 @@ using Barotrauma.Networking; +using System.Collections.Generic; namespace Barotrauma { - partial class SalvageMission : Mission + partial class SalvageMission : Mission { + private bool usedExistingItem; + + private readonly List> executedEffectIndices = new List>(); + public override void ServerWriteInitial(IWriteMessage msg, Client c) { msg.Write(usedExistingItem); @@ -13,7 +18,14 @@ namespace Barotrauma } else { - item.WriteSpawnData(msg, item.ID); + item.WriteSpawnData(msg, item.ID, item.ParentInventory?.Owner?.ID ?? 0); + } + + msg.Write((byte)executedEffectIndices.Count); + foreach (Pair effectIndex in executedEffectIndices) + { + msg.Write((byte)effectIndex.First); + msg.Write((byte)effectIndex.Second); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index 3ce575eb9..008a79402 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -6,7 +6,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Reflection; using System.Threading; @@ -105,6 +105,8 @@ namespace Barotrauma MapGenerationParams.Init(); LevelGenerationParams.LoadPresets(); ScriptedEventSet.LoadPrefabs(); + Order.Init(); + EventManagerSettings.Init(); AfflictionPrefab.LoadAll(GetFilesOfType(ContentType.Afflictions)); SkillSettings.Load(GetFilesOfType(ContentType.SkillSettings)); @@ -180,7 +182,7 @@ namespace Barotrauma bool enableUpnp = false; int maxPlayers = 10; - int ownerKey = 0; + int? ownerKey = null; UInt64 steamId = 0; XDocument doc = XMLExtensions.TryLoadXml(ServerSettings.SettingsFile); @@ -197,7 +199,7 @@ namespace Barotrauma password = doc.Root.GetAttributeString("password", ""); enableUpnp = doc.Root.GetAttributeBool("enableupnp", false); maxPlayers = doc.Root.GetAttributeInt("maxplayers", 10); - ownerKey = 0; + ownerKey = null; } #if DEBUG @@ -244,7 +246,10 @@ namespace Barotrauma i++; break; case "-ownerkey": - int.TryParse(CommandLineArgs[i + 1], out ownerKey); + if (int.TryParse(CommandLineArgs[i + 1], out int key)) + { + ownerKey = key; + } i++; break; case "-steamid": diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 2fc35dd0d..d731a30bd 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -1,4 +1,5 @@ -using Barotrauma.Networking; +using Barotrauma.IO; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -189,7 +190,7 @@ namespace Barotrauma foreach (PurchasedItem pi in CargoManager.PurchasedItems) { msg.Write(pi.ItemPrefab.Identifier); - msg.Write((UInt16)pi.Quantity); + msg.WriteRangedInteger(pi.Quantity, 0, 100); } var characterData = GetClientCharacterData(c); @@ -217,7 +218,7 @@ namespace Barotrauma for (int i = 0; i < purchasedItemCount; i++) { string itemPrefabIdentifier = msg.ReadString(); - UInt16 itemQuantity = msg.ReadUInt16(); + int itemQuantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity); purchasedItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity)); } @@ -255,8 +256,8 @@ namespace Barotrauma } if (purchasedLostShuttles != this.PurchasedLostShuttles) { - if (GameMain.GameSession?.Submarine != null && - GameMain.GameSession.Submarine.LeftBehindSubDockingPortOccupied) + if (GameMain.GameSession?.SubmarineInfo != null && + GameMain.GameSession.SubmarineInfo.LeftBehindSubDockingPortOccupied) { GameMain.Server.SendDirectChatMessage(TextManager.FormatServerMessage("ReplaceShuttleDockingPortOccupied"), sender, ChatMessageType.MessageBox); } @@ -306,7 +307,7 @@ namespace Barotrauma } try { - characterDataDoc.Save(characterDataPath); + characterDataDoc.SaveSafe(characterDataPath); } catch (Exception e) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs index 08c4eed64..7072e5b2b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Door.cs @@ -15,7 +15,7 @@ namespace Barotrauma.Items.Components isOpen = open; //opening a partially stuck door makes it less stuck - if (isOpen) stuck = MathHelper.Clamp(stuck - 30.0f, 0.0f, 100.0f); + if (isOpen) { stuck = MathHelper.Clamp(stuck - StuckReductionOnOpen, 0.0f, 100.0f); } if (sendNetworkMessage) { @@ -28,6 +28,7 @@ namespace Barotrauma.Items.Components base.ServerWrite(msg, c, extraData); msg.Write(isOpen); + msg.Write(isBroken); msg.Write(extraData.Length == 3 ? (bool)extraData[2] : false); //forced open msg.WriteRangedSingle(stuck, 0.0f, 100.0f, 8); msg.Write(lastUser == null ? (UInt16)0 : lastUser.ID); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs index 4ad1b84cd..8d1de81ae 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs @@ -13,6 +13,7 @@ namespace Barotrauma.Items.Components msg.Write(Attached); msg.Write(body.SimPosition.X); msg.Write(body.SimPosition.Y); + msg.Write(item.Submarine?.ID ?? Entity.NullEntityID); } public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) @@ -30,7 +31,7 @@ namespace Barotrauma.Items.Components AttachToWall(); item.CreateServerEvent(this); - GameServer.Log(c.Character.LogName + " attached " + item.Name + " to a wall", ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(c.Character) + " attached " + item.Name + " to a wall", ServerLog.MessageType.ItemInteraction); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Controller.cs index 7bdbac370..5de5d4360 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Controller.cs @@ -6,7 +6,7 @@ namespace Barotrauma.Items.Components { public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { - msg.Write(state); + msg.Write(State); msg.Write(user == null ? (ushort)0 : user.ID); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Engine.cs index af8583204..d39ad733f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Engine.cs @@ -19,7 +19,7 @@ namespace Barotrauma.Items.Components { if (Math.Abs(newTargetForce - targetForce) > 0.01f) { - GameServer.Log(c.Character.LogName + " set the force of " + item.Name + " to " + (int)(newTargetForce) + " %", ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(c.Character) + " set the force of " + item.Name + " to " + (int)(newTargetForce) + " %", ServerLog.MessageType.ItemInteraction); } targetForce = newTargetForce; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs index 4b8261204..903b05330 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs @@ -17,11 +17,11 @@ namespace Barotrauma.Items.Components { if (newFlowPercentage != FlowPercentage) { - GameServer.Log(c.Character.LogName + " set the pumping speed of " + item.Name + " to " + (int)(newFlowPercentage) + " %", ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(c.Character) + " set the pumping speed of " + item.Name + " to " + (int)(newFlowPercentage) + " %", ServerLog.MessageType.ItemInteraction); } if (newIsActive != IsActive) { - GameServer.Log(c.Character.LogName + (newIsActive ? " turned on " : " turned off ") + item.Name, ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(c.Character) + (newIsActive ? " turned on " : " turned off ") + item.Name, ServerLog.MessageType.ItemInteraction); } FlowPercentage = newFlowPercentage; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Power/PowerContainer.cs index 8b9acaaf8..20ae5b275 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Power/PowerContainer.cs @@ -14,7 +14,7 @@ namespace Barotrauma.Items.Components if (item.CanClientAccess(c)) { RechargeSpeed = newRechargeSpeed; - GameServer.Log(c.Character.LogName + " set the recharge speed of " + item.Name + " to " + (int)((rechargeSpeed / maxRechargeSpeed) * 100.0f) + " %", ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(c.Character) + " set the recharge speed of " + item.Name + " to " + (int)((rechargeSpeed / maxRechargeSpeed) * 100.0f) + " %", ServerLog.MessageType.ItemInteraction); } item.CreateServerEvent(this); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs index 6c06d44d9..bc69285de 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs @@ -7,17 +7,21 @@ namespace Barotrauma.Items.Components { public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { - msg.Write(StickTarget != null); - if (StickTarget != null) + bool stuck = StickTarget != null && !item.Removed && !StickTargetRemoved(); + msg.Write(stuck); + if (stuck) { - msg.Write(item.body.SimPosition.X); - msg.Write(item.body.SimPosition.Y); + msg.Write(item.Submarine?.ID ?? Entity.NullEntityID); + msg.Write(item.CurrentHull?.ID ?? Entity.NullEntityID); + msg.Write(item.SimPosition.X); + msg.Write(item.SimPosition.Y); msg.Write(stickJoint.Axis.X); msg.Write(stickJoint.Axis.Y); if (StickTarget.UserData is Structure structure) { msg.Write(structure.ID); - msg.Write((byte)structure.Bodies.IndexOf(StickTarget)); + int bodyIndex = structure.Bodies.IndexOf(StickTarget); + msg.Write((byte)(bodyIndex == -1 ? 0 : bodyIndex)); } else if (StickTarget.UserData is Entity entity) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs index d4187a717..d29f38b98 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs @@ -88,7 +88,7 @@ namespace Barotrauma.Items.Components if (existingWire.Locked) { //this should not be possible unless the client is running a modified version of the game - GameServer.Log(c.Character.LogName + " attempted to disconnect a locked wire from " + + GameServer.Log(GameServer.CharacterLogName(c.Character) + " attempted to disconnect a locked wire from " + Connections[i].Item.Name + " (" + Connections[i].Name + ")", ServerLog.MessageType.Error); continue; } @@ -106,7 +106,7 @@ namespace Barotrauma.Items.Components if (existingWire.Connections[0] == null && existingWire.Connections[1] == null) { - GameServer.Log(c.Character.LogName + " disconnected a wire from " + + GameServer.Log(GameServer.CharacterLogName(c.Character) + " disconnected a wire from " + Connections[i].Item.Name + " (" + Connections[i].Name + ")", ServerLog.MessageType.Wiring); if (existingWire.Item.ParentInventory != null) @@ -122,7 +122,7 @@ namespace Barotrauma.Items.Components } else if (existingWire.Connections[0] != null) { - GameServer.Log(c.Character.LogName + " disconnected a wire from " + + GameServer.Log(GameServer.CharacterLogName(c.Character) + " disconnected a wire from " + Connections[i].Item.Name + " (" + Connections[i].Name + ") to " + existingWire.Connections[0].Item.Name + " (" + existingWire.Connections[0].Name + ")", ServerLog.MessageType.Wiring); //wires that are not in anyone's inventory (i.e. not currently being rewired) @@ -137,7 +137,7 @@ namespace Barotrauma.Items.Components } else if (existingWire.Connections[1] != null) { - GameServer.Log(c.Character.LogName + " disconnected a wire from " + + GameServer.Log(GameServer.CharacterLogName(c.Character) + " disconnected a wire from " + Connections[i].Item.Name + " (" + Connections[i].Name + ") to " + existingWire.Connections[1].Item.Name + " (" + existingWire.Connections[1].Name + ")", ServerLog.MessageType.Wiring); /*if (existingWire.Item.ParentInventory == null && !wires.Any(w => w.Contains(existingWire))) @@ -161,7 +161,7 @@ namespace Barotrauma.Items.Components disconnectedWire.Item.ParentInventory == null) { disconnectedWire.Item.Drop(c.Character); - GameServer.Log(c.Character.LogName + " dropped " + disconnectedWire.Name, ServerLog.MessageType.Inventory); + GameServer.Log(GameServer.CharacterLogName(c.Character) + " dropped " + disconnectedWire.Name, ServerLog.MessageType.Inventory); } } @@ -180,13 +180,13 @@ namespace Barotrauma.Items.Components if (otherConnection == null) { - GameServer.Log(c.Character.LogName + " connected a wire to " + + GameServer.Log(GameServer.CharacterLogName(c.Character) + " connected a wire to " + Connections[i].Item.Name + " (" + Connections[i].Name + ")", ServerLog.MessageType.Wiring); } else { - GameServer.Log(c.Character.LogName + " connected a wire from " + + GameServer.Log(GameServer.CharacterLogName(c.Character) + " connected a wire from " + Connections[i].Item.Name + " (" + Connections[i].Name + ") to " + (otherConnection == null ? "none" : otherConnection.Item.Name + " (" + (otherConnection.Name) + ")"), ServerLog.MessageType.Wiring); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs index 58c896d2e..4d15cc9e2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs @@ -24,7 +24,7 @@ namespace Barotrauma.Items.Components } CustomInterfaceElement clickedButton = null; - if (item.CanClientAccess(c)) + if ((c.Character != null && DrawHudWhenEquipped && item.ParentInventory?.Owner == c.Character) || item.CanClientAccess(c)) { for (int i = 0; i < customInterfaceElementList.Count; i++) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs index 7bd88be3d..1950c4adf 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs @@ -14,7 +14,7 @@ namespace Barotrauma.Items.Components { newOutputValue = newOutputValue.Substring(0, MaxMessageLength); } - GameServer.Log(c.Character.LogName + " entered \"" + newOutputValue + "\" on " + item.Name, + GameServer.Log(GameServer.CharacterLogName(c.Character) + " entered \"" + newOutputValue + "\" on " + item.Name, ServerLog.MessageType.ItemInteraction); OutputValue = newOutputValue; item.SendSignal(0, newOutputValue, "signal_out", null); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs index c1fe0a535..0574ec775 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs @@ -66,6 +66,19 @@ namespace Barotrauma Item droppedItem = Items[i]; Entity prevOwner = Owner; droppedItem.Drop(null); + + var previousInventory = prevOwner switch + { + Item itemInventory => (itemInventory.FindParentInventory(inventory => inventory is CharacterInventory) as CharacterInventory), + Character character => character.Inventory, + _ => null + }; + + if (previousInventory != null && previousInventory != c.Character?.Inventory) + { + GameMain.Server?.KarmaManager.OnItemTakenFromPlayer(previousInventory, c, droppedItem); + } + if (droppedItem.body != null && prevOwner != null) { droppedItem.body.SetTransform(prevOwner.SimPosition, 0.0f); @@ -120,11 +133,11 @@ namespace Barotrauma { if (Owner == c.Character) { - GameServer.Log(c.Character.LogName+ " picked up " + item.Name, ServerLog.MessageType.Inventory); + GameServer.Log(GameServer.CharacterLogName(c.Character) + " picked up " + item.Name, ServerLog.MessageType.Inventory); } else { - GameServer.Log(c.Character.LogName + " placed " + item.Name + " in " + Owner, ServerLog.MessageType.Inventory); + GameServer.Log(GameServer.CharacterLogName(c.Character) + " placed " + item.Name + " in " + Owner, ServerLog.MessageType.Inventory); } } } @@ -135,11 +148,11 @@ namespace Barotrauma { if (Owner == c.Character) { - GameServer.Log(c.Character.LogName + " dropped " + item.Name, ServerLog.MessageType.Inventory); + GameServer.Log(GameServer.CharacterLogName(c.Character) + " dropped " + item.Name, ServerLog.MessageType.Inventory); } else { - GameServer.Log(c.Character.LogName + " removed " + item.Name + " from " + Owner, ServerLog.MessageType.Inventory); + GameServer.Log(GameServer.CharacterLogName(c.Character) + " removed " + item.Name + " from " + Owner, ServerLog.MessageType.Inventory); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index 826ff1260..db01d330e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -186,12 +186,12 @@ namespace Barotrauma if (ContainedItems == null || ContainedItems.All(i => i == null)) { - GameServer.Log(c.Character.LogName + " used item " + Name, ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(c.Character) + " used item " + Name, ServerLog.MessageType.ItemInteraction); } else { GameServer.Log( - c.Character.LogName + " used item " + Name + " (contained items: " + string.Join(", ", ContainedItems.Select(i => i.Name)) + ")", + GameServer.CharacterLogName(c.Character) + " used item " + Name + " (contained items: " + string.Join(", ", ContainedItems.Select(i => i.Name)) + ")", ServerLog.MessageType.ItemInteraction); } @@ -213,7 +213,7 @@ namespace Barotrauma } } - public void WriteSpawnData(IWriteMessage msg, UInt16 entityID) + public void WriteSpawnData(IWriteMessage msg, UInt16 entityID, UInt16 originalInventoryID) { if (GameMain.Server == null) return; @@ -227,7 +227,7 @@ namespace Barotrauma msg.Write(entityID); - if (ParentInventory == null || ParentInventory.Owner == null) + if (ParentInventory == null || ParentInventory.Owner == null || originalInventoryID == 0) { msg.Write((ushort)0); @@ -237,7 +237,7 @@ namespace Barotrauma } else { - msg.Write(ParentInventory.Owner.ID); + msg.Write(originalInventoryID); //find the index of the ItemContainer this item is inside to get the item to //spawn in the correct inventory in multi-inventory items like fabricators diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs index 0924c5e72..e02c6ba3f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Net; @@ -347,7 +347,7 @@ namespace Barotrauma.Networking BannedPlayer bannedPlayer = bannedPlayers.Find(p => p.UniqueIdentifier == id); if (bannedPlayer != null) { - GameServer.Log(c.Name + " unbanned " + bannedPlayer.Name + " (" + bannedPlayer.IP + ")", ServerLog.MessageType.ConsoleUsage); + GameServer.Log(GameServer.ClientLogName(c) + " unbanned " + bannedPlayer.Name + " (" + bannedPlayer.IP + ")", ServerLog.MessageType.ConsoleUsage); RemoveBan(bannedPlayer); } } @@ -358,7 +358,7 @@ namespace Barotrauma.Networking BannedPlayer bannedPlayer = bannedPlayers.Find(p => p.UniqueIdentifier == id); if (bannedPlayer != null) { - GameServer.Log(c.Name + " rangebanned " + bannedPlayer.Name + " (" + bannedPlayer.IP + ")", ServerLog.MessageType.ConsoleUsage); + GameServer.Log(GameServer.ClientLogName(c) + " rangebanned " + bannedPlayer.Name + " (" + bannedPlayer.IP + ")", ServerLog.MessageType.ConsoleUsage); RangeBan(bannedPlayer); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs index 84044b2af..e2468f5cc 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs @@ -159,6 +159,7 @@ namespace Barotrauma.Networking msg.Write((byte)ServerNetObject.CHAT_MESSAGE); msg.Write(NetStateID); msg.Write((byte)Type); + msg.Write((byte)ChangeType); msg.Write(Text); msg.Write(SenderName); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChildServerRelay.cs index 1a1bbb96b..6226d0b2c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChildServerRelay.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; +using Barotrauma.IO; using System.IO.Pipes; using System.Text; using System.Threading; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs index 0c60acc5b..7d30c1db1 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs @@ -75,7 +75,8 @@ namespace Barotrauma.Networking public bool SpectateOnly; public int KarmaKickCount; - + + private float syncedKarma = 100.0f; private float karma = 100.0f; public float Karma { @@ -89,6 +90,11 @@ namespace Barotrauma.Networking { if (GameMain.Server == null || !GameMain.Server.ServerSettings.KarmaEnabled) { return; } karma = Math.Min(Math.Max(value, 0.0f), 100.0f); + if (!MathUtils.NearlyEqual(karma, syncedKarma, 10.0f)) + { + syncedKarma = karma; + GameMain.NetworkMember.LastClientListUpdateID++; + } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/EntitySpawner.cs index ac4111dea..b43825dde 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/EntitySpawner.cs @@ -35,7 +35,7 @@ namespace Barotrauma { message.Write((byte)SpawnableType.Item); DebugConsole.Log("Writing item spawn data " + entities.Entity.ToString() + " (original ID: " + entities.OriginalID + ", current ID: " + entities.Entity.ID + ")"); - ((Item)entities.Entity).WriteSpawnData(message, entities.OriginalID); + ((Item)entities.Entity).WriteSpawnData(message, entities.OriginalID, entities.OriginalInventoryID); } else if (entities.Entity is Character) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs index 74704c129..c2746718e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs @@ -1,7 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Threading; @@ -93,7 +93,7 @@ namespace Barotrauma.Networking { data = File.ReadAllBytes(filePath); } - catch (IOException e) + catch (System.IO.IOException e) { if (i >= maxRetries) { throw; } DebugConsole.NewMessage("Failed to initiate a file transfer {" + e.Message + "}, retrying in 250 ms...", Color.Red); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 05e174ce6..ee5a57d39 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -8,7 +8,7 @@ using System.Diagnostics; using System.Linq; using System.Text; using System.IO.Compression; -using System.IO; +using Barotrauma.IO; using Barotrauma.Steam; using System.Xml.Linq; using System.Threading; @@ -279,7 +279,7 @@ namespace Barotrauma.Networking SendConsoleMessage("Granted all permissions to " + newClient.Name + ".", newClient); } - SendChatMessage($"ServerMessage.JoinedServer~[client]={clName}", ChatMessageType.Server, null); + SendChatMessage($"ServerMessage.JoinedServer~[client]={clName}", ChatMessageType.Server, null, changeType: PlayerConnectionChangeType.Joined); serverSettings.ServerDetailsChanged = true; if (previousPlayer != null && previousPlayer.Name != newClient.Name) @@ -338,6 +338,8 @@ namespace Barotrauma.Networking fileSender.Update(deltaTime); KarmaManager.UpdateClients(ConnectedClients, deltaTime); + UpdatePing(); + if (serverSettings.VoiceChatEnabled) { VoipServer.SendToClients(connectedClients); @@ -611,6 +613,41 @@ namespace Barotrauma.Networking } } + + private double lastPingTime; + private byte[] lastPingData; + private void UpdatePing() + { + if (Timing.TotalTime > lastPingTime + 1.0) + { + lastPingData ??= new byte[64]; + for (int i=0;i + { + IWriteMessage pingReq = new WriteOnlyMessage(); + pingReq.Write((byte)ServerPacketHeader.PING_REQUEST); + pingReq.Write((byte)lastPingData.Length); + pingReq.Write(lastPingData, 0, lastPingData.Length); + serverPeer.Send(pingReq, c.Connection, DeliveryMethod.Unreliable); + + IWriteMessage pingInf = new WriteOnlyMessage(); + pingInf.Write((byte)ServerPacketHeader.CLIENT_PINGS); + pingInf.Write((byte)ConnectedClients.Count); + ConnectedClients.ForEach(c2 => + { + pingInf.Write(c2.ID); + pingInf.Write(c2.Ping); + }); + serverPeer.Send(pingInf, c.Connection, DeliveryMethod.Unreliable); + }); + } + } + private void ReadDataMessage(NetworkConnection sender, IReadMessage inc) { var connectedClient = connectedClients.Find(c => c.Connection == sender); @@ -618,6 +655,16 @@ namespace Barotrauma.Networking ClientPacketHeader header = (ClientPacketHeader)inc.ReadByte(); switch (header) { + case ClientPacketHeader.PING_RESPONSE: + byte responseLen = inc.ReadByte(); + if (responseLen != lastPingData.Length) { return; } + for (int i=0;i cl != sender && cl.Name.Equals(kickedName, StringComparison.OrdinalIgnoreCase) && cl.Connection != OwnerConnection); if (kickedClient != null) { - Log("Client \"" + sender.Name + "\" kicked \"" + kickedClient.Name + "\".", ServerLog.MessageType.ServerMessage); + Log("Client \"" + GameServer.ClientLogName(sender) + "\" kicked \"" + GameServer.ClientLogName(kickedClient) + "\".", ServerLog.MessageType.ServerMessage); KickClient(kickedClient, string.IsNullOrEmpty(kickReason) ? $"ServerMessage.KickedBy~[initiator]={sender.Name}" : kickReason); } else @@ -1097,7 +1156,7 @@ namespace Barotrauma.Networking var bannedClient = connectedClients.Find(cl => cl != sender && cl.Name.Equals(bannedName, StringComparison.OrdinalIgnoreCase) && cl.Connection != OwnerConnection); if (bannedClient != null) { - Log("Client \"" + sender.Name + "\" banned \"" + bannedClient.Name + "\".", ServerLog.MessageType.ServerMessage); + Log("Client \"" + GameServer.ClientLogName(sender) + "\" banned \"" + GameServer.ClientLogName(bannedClient) + "\".", ServerLog.MessageType.ServerMessage); if (durationSeconds > 0) { BanClient(bannedClient, string.IsNullOrEmpty(banReason) ? $"ServerMessage.BannedBy~[initiator]={sender.Name}" : banReason, range, TimeSpan.FromSeconds(durationSeconds)); @@ -1121,12 +1180,12 @@ namespace Barotrauma.Networking bool end = inc.ReadBoolean(); if (gameStarted && end) { - Log("Client \"" + sender.Name + "\" ended the round.", ServerLog.MessageType.ServerMessage); + Log("Client \"" + GameServer.ClientLogName(sender) + "\" ended the round.", ServerLog.MessageType.ServerMessage); EndGame(); } else if (!gameStarted && !end && !initiatedStartGame) { - Log("Client \"" + sender.Name + "\" started the round.", ServerLog.MessageType.ServerMessage); + Log("Client \"" + GameServer.ClientLogName(sender) + "\" started the round.", ServerLog.MessageType.ServerMessage); StartGame(); } break; @@ -1137,7 +1196,7 @@ namespace Barotrauma.Networking var subList = GameMain.NetLobbyScreen.GetSubList(); if (subIndex >= subList.Count) { - DebugConsole.NewMessage("Client \"" + sender.Name + "\" attempted to select a sub, index out of bounds (" + subIndex + ")", Color.Red); + DebugConsole.NewMessage("Client \"" + GameServer.ClientLogName(sender) + "\" attempted to select a sub, index out of bounds (" + subIndex + ")", Color.Red); } else { @@ -1220,12 +1279,12 @@ namespace Barotrauma.Networking string logMsg; if (permissionNames.Any()) { - logMsg = "Client \"" + sender.Name + "\" set the permissions of the client \"" + targetClient.Name + "\" to " + logMsg = "Client \"" + GameServer.ClientLogName(sender) + "\" set the permissions of the client \"" + GameServer.ClientLogName(targetClient) + "\" to " + string.Join(", ", permissionNames); } else { - logMsg = "Client \"" + sender.Name + "\" removed all permissions from the client \"" + targetClient.Name + "."; + logMsg = "Client \"" + GameServer.ClientLogName(sender) + "\" removed all permissions from the client \"" + GameServer.ClientLogName(targetClient) + "."; } Log(logMsg, ServerLog.MessageType.ServerMessage); @@ -1484,9 +1543,21 @@ namespace Barotrauma.Networking outmsg.Write(client.Name); outmsg.Write(client.Character == null || !gameStarted ? (client.PreferredJob ?? "") : ""); outmsg.Write(client.Character == null || !gameStarted ? (ushort)0 : client.Character.ID); + if (c.HasPermission(ClientPermissions.ServerLog)) + { + outmsg.Write(client.Karma); + } + else + { + outmsg.Write(100.0f); + } outmsg.Write(client.Muted); outmsg.Write(client.InGame); - outmsg.Write(client.Connection != OwnerConnection); //is kicking the player allowed + outmsg.Write(client.Permissions != ClientPermissions.None); + outmsg.Write(client.Connection != OwnerConnection && + !client.HasPermission(ClientPermissions.Ban) && + !client.HasPermission(ClientPermissions.Kick) && + !client.HasPermission(ClientPermissions.Unban)); //is kicking the player allowed outmsg.WritePadBits(); } } @@ -1846,6 +1917,15 @@ namespace Barotrauma.Networking var teamID = n == 0 ? Character.TeamType.Team1 : Character.TeamType.Team2; Submarine.MainSubs[n].TeamID = teamID; + foreach (Item item in Item.ItemList) + { + if (item.Submarine == null) { continue; } + if (item.Submarine != Submarine.MainSubs[n] && !Submarine.MainSubs[n].DockedTo.Contains(item.Submarine)) { continue; } + foreach (WifiComponent wifiComponent in item.GetComponents()) + { + wifiComponent.TeamID = Submarine.MainSubs[n].TeamID; + } + } foreach (Submarine sub in Submarine.MainSubs[n].DockedTo) { sub.TeamID = teamID; @@ -2139,7 +2219,16 @@ namespace Barotrauma.Networking public override void AddChatMessage(ChatMessage message) { if (string.IsNullOrEmpty(message.Text)) { return; } - Log(message.TextWithSender, ServerLog.MessageType.Chat); + string logMsg; + if (message.SenderClient != null) + { + logMsg = GameServer.ClientLogName(message.SenderClient) + ": " + message.TranslatedText; + } + else + { + logMsg = message.TextWithSender; + } + Log(logMsg, ServerLog.MessageType.Chat); base.AddChatMessage(message); } @@ -2223,7 +2312,7 @@ namespace Barotrauma.Networking string msg = DisconnectReason.Kicked.ToString(); string logMsg = $"ServerMessage.KickedFromServer~[client]={client.Name}"; - DisconnectClient(client, logMsg, msg, reason); + DisconnectClient(client, logMsg, msg, reason, PlayerConnectionChangeType.Kicked); } public override void BanPlayer(string playerName, string reason, bool range = false, TimeSpan? duration = null) @@ -2254,7 +2343,7 @@ namespace Barotrauma.Networking client.Karma = Math.Max(client.Karma, 50.0f); string targetMsg = DisconnectReason.Banned.ToString(); - DisconnectClient(client, $"ServerMessage.BannedFromServer~[client]={client.Name}", targetMsg, reason); + DisconnectClient(client, $"ServerMessage.BannedFromServer~[client]={client.Name}", targetMsg, reason, PlayerConnectionChangeType.Banned); if (client.SteamID == 0 || range) { @@ -2298,10 +2387,10 @@ namespace Barotrauma.Networking Client client = connectedClients.Find(x => x.Connection == senderConnection); if (client == null) return; - DisconnectClient(client, msg, targetmsg, string.Empty); + DisconnectClient(client, msg, targetmsg, string.Empty, PlayerConnectionChangeType.Disconnected); } - public void DisconnectClient(Client client, string msg = "", string targetmsg = "", string reason = "") + public void DisconnectClient(Client client, string msg = "", string targetmsg = "", string reason = "", PlayerConnectionChangeType changeType = PlayerConnectionChangeType.Disconnected) { if (client == null) return; @@ -2348,7 +2437,7 @@ namespace Barotrauma.Networking UpdateVoteStatus(); - SendChatMessage(msg, ChatMessageType.Server); + SendChatMessage(msg, ChatMessageType.Server, changeType: changeType); UpdateCrewFrame(); @@ -2397,7 +2486,7 @@ namespace Barotrauma.Networking /// /// Add the message to the chatbox and pass it to all clients who can receive it /// - public void SendChatMessage(string message, ChatMessageType? type = null, Client senderClient = null, Character senderCharacter = null) + public void SendChatMessage(string message, ChatMessageType? type = null, Client senderClient = null, Character senderCharacter = null, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None) { string senderName = ""; @@ -2482,17 +2571,20 @@ namespace Barotrauma.Networking senderCharacter = senderClient.Character; senderName = senderCharacter == null ? senderClient.Name : senderCharacter.Name; + if (type == ChatMessageType.Private) + { + if (senderCharacter != null && !senderCharacter.IsDead || targetClient.Character != null && !targetClient.Character.IsDead) + { + //sender or target has an alive character, sending private messages not allowed + SendDirectChatMessage(ChatMessage.Create("", $"ServerMessage.PrivateMessagesNotAllowed", ChatMessageType.Error, null), senderClient); + return; + } + } //sender doesn't have a character or the character can't speak -> only ChatMessageType.Dead allowed - if (senderCharacter == null || senderCharacter.IsDead || senderCharacter.SpeechImpediment >= 100.0f) + else if (senderCharacter == null || senderCharacter.IsDead || senderCharacter.SpeechImpediment >= 100.0f) { type = ChatMessageType.Dead; } - else if (type == ChatMessageType.Private) - { - //sender has an alive character, sending private messages not allowed - return; - } - } } else @@ -2588,7 +2680,9 @@ namespace Barotrauma.Networking senderName, modifiedMessage, (ChatMessageType)type, - senderCharacter); + senderCharacter, + senderClient, + changeType); SendDirectChatMessage(chatMsg, client); } @@ -2659,6 +2753,9 @@ namespace Barotrauma.Networking var clientsToKick = connectedClients.FindAll(c => c.Connection != OwnerConnection && + !c.HasPermission(ClientPermissions.Kick) && + !c.HasPermission(ClientPermissions.Ban) && + !c.HasPermission(ClientPermissions.Unban) && c.KickVoteCount >= connectedClients.Count * serverSettings.KickVoteRequiredRatio); foreach (Client c in clientsToKick) { @@ -2669,7 +2766,7 @@ namespace Barotrauma.Networking previousPlayer.KickVoters.Clear(); } - SendChatMessage($"ServerMessage.KickedFromServer~[client]={c.Name}", ChatMessageType.Server, null); + SendChatMessage($"ServerMessage.KickedFromServer~[client]={c.Name}", ChatMessageType.Server, null, changeType: PlayerConnectionChangeType.Kicked); KickClient(c, "ServerMessage.KickedByVote"); BanClient(c, "ServerMessage.KickedByVoteAutoBan", duration: TimeSpan.FromSeconds(serverSettings.AutoBanTime)); } @@ -2853,6 +2950,11 @@ namespace Barotrauma.Networking { newCharacter.LastNetworkUpdateID = client.Character.LastNetworkUpdateID; } + + if (newCharacter.Info != null && newCharacter.Info.Character == null) + { + newCharacter.Info.Character = newCharacter; + } newCharacter.OwnerClientEndPoint = client.Connection.EndPointString; newCharacter.OwnerClientName = client.Name; @@ -3199,6 +3301,25 @@ namespace Barotrauma.Networking } } + public static string ClientLogName(Client client, string name = null) + { + if (client == null) { return name; } + string retVal = "‖"; + if (client.Karma < 40.0f) + { + retVal += "color:#ff9900;"; + } + retVal += "metadata:" + (client.SteamID!=0 ? client.SteamID.ToString() : client.ID.ToString()) + "‖" + (name ?? client.Name) + "‖end‖"; + return retVal; + } + + public static string CharacterLogName(Character character) + { + if (character == null) { return "[NULL]"; } + Client client = GameMain.Server.ConnectedClients.Find(c => c.Character == character); + return ClientLogName(client, character.LogName); + } + public static void Log(string line, ServerLog.MessageType messageType) { if (GameMain.Server == null || !GameMain.Server.ServerSettings.SaveServerLogs) return; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs index 8b5227fdb..1ecbbab59 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs @@ -3,6 +3,7 @@ using Barotrauma.Networking; using System; using System.Collections.Generic; using System.Linq; +using Barotrauma.Extensions; namespace Barotrauma { @@ -12,8 +13,18 @@ namespace Barotrauma { public List> WireDisconnectTime = new List>(); + public struct TimeAmount + { + public double Time; + public float Amount; + } + + public List KarmaDecreasesInPastMinute = new List(); + public float PreviousNotifiedKarma; + public double PreviousKarmaNotificationTime; + public float StructureDamageAccumulator; private float structureDamagePerSecond; @@ -23,6 +34,9 @@ namespace Barotrauma set { structureDamagePerSecond = value; } } + public List StunsInPastMinute = new List(); + public float StunKarmaDecreaseMultiplier; + //when did a given character last attack this one public Dictionary LastAttackTime { @@ -53,6 +67,13 @@ namespace Barotrauma clientMemory.StructureDamagePerSecond = clientMemory.StructureDamageAccumulator; clientMemory.StructureDamageAccumulator = 0.0f; + clientMemory.StunsInPastMinute.RemoveAll(s => s.Time + 60.0f < Timing.TotalTime); + + if (!clientMemory.StunsInPastMinute.Any()) + { + clientMemory.StunKarmaDecreaseMultiplier = 1.0f; + } + var toRemove = clientMemory.LastAttackTime.Where(pair => pair.Value < Timing.TotalTime - AllowedRetaliationTime).Select(pair => pair.Key).ToList(); foreach (var lastAttacker in toRemove) { @@ -89,28 +110,26 @@ namespace Barotrauma var clientMemory = GetClientMemory(client); float karmaChange = client.Karma - clientMemory.PreviousNotifiedKarma; - if (Math.Abs(karmaChange) > 1.0f && - (TestMode || Math.Abs(karmaChange) / clientMemory.PreviousNotifiedKarma > KarmaNotificationInterval / 100.0f)) + if (Math.Abs(karmaChange) > 1.0f && TestMode) { - if (TestMode) + string msg = + karmaChange < 0 ? $"Your karma has decreased to {client.Karma}" : $"Your karma has increased to {client.Karma}"; + if (!string.IsNullOrEmpty(debugKarmaChangeReason)) { - string msg = - karmaChange < 0 ? $"Your karma has decreased to {client.Karma}" : $"Your karma has increased to {client.Karma}"; - if (!string.IsNullOrEmpty(debugKarmaChangeReason)) - { - msg += $". Reason: {debugKarmaChangeReason}"; - } - GameMain.Server.SendDirectChatMessage(msg, client); + msg += $". Reason: {debugKarmaChangeReason}"; } - else if (Math.Abs(KickBanThreshold - client.Karma) < KarmaNotificationInterval) - { - GameMain.Server.SendDirectChatMessage(TextManager.Get("KarmaBanWarning"), client); - } - else - { - GameMain.Server.SendDirectChatMessage(TextManager.Get(karmaChange < 0 ? "KarmaDecreasedUnknownAmount" : "KarmaIncreasedUnknownAmount"), client); - } - clientMemory.PreviousNotifiedKarma = client.Karma; + GameMain.Server.SendDirectChatMessage(msg, client); + clientMemory.PreviousNotifiedKarma = client.Karma; + clientMemory.PreviousKarmaNotificationTime = Timing.TotalTime; + } + else if (Timing.TotalTime >= clientMemory.PreviousKarmaNotificationTime + 5.0f && + clientMemory.PreviousNotifiedKarma >= KickBanThreshold + KarmaNotificationInterval && + client.Karma < KickBanThreshold + KarmaNotificationInterval) + { + GameMain.Server.SendDirectChatMessage(TextManager.Get("KarmaBanWarning"), client); + GameServer.Log(GameServer.ClientLogName(client) + " has been warned for having dangerously low karma.", ServerLog.MessageType.Karma); + clientMemory.PreviousNotifiedKarma = client.Karma; + clientMemory.PreviousKarmaNotificationTime = Timing.TotalTime; } } @@ -130,10 +149,10 @@ namespace Barotrauma //increase the strength of the herpes affliction in steps instead of linearly //otherwise clients could determine their exact karma value from the strength float herpesStrength = 0.0f; - if (client.Karma < 20) - herpesStrength = 100.0f; - else if (client.Karma < 30) - herpesStrength = 60.0f; + if (client.Karma < 20) + herpesStrength = 100.0f; + else if (client.Karma < 30) + herpesStrength = 60.0f; else if (client.Karma < 40.0f) herpesStrength = 30.0f; @@ -141,6 +160,8 @@ namespace Barotrauma if (existingAffliction == null && herpesStrength > 0.0f) { client.Character.CharacterHealth.ApplyAffliction(null, new Affliction(herpesAffliction, herpesStrength)); + GameServer.Log($"{GameServer.ClientLogName(client)} has contracted space herpes due to low karma.", ServerLog.MessageType.Karma); + GameMain.NetworkMember.LastClientListUpdateID++; } else if (existingAffliction != null) { @@ -205,7 +226,84 @@ namespace Barotrauma clientMemories.Remove(client); } - public void OnCharacterHealthChanged(Character target, Character attacker, float damage, IEnumerable appliedAfflictions = null) + // ReSharper disable once UseNegatedPatternMatching, LoopCanBeConvertedToQuery + public void OnItemTakenFromPlayer(CharacterInventory inventory, Client yoinker, Item item) + { + Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == inventory.Owner); + + Character yoinkerCharacter = yoinker?.Character; + Character targetCharacter = inventory.Owner as Character; + + if (yoinker == null || item == null || yoinkerCharacter == null || targetCharacter == null || yoinkerCharacter == targetCharacter) { return; } + + if (targetClient == null && (!DangerousItemStealBots || targetCharacter.AIController == null)) { return; } + + // Only if the target is alive and they are stunned, unconscious or handcuffed + if (targetCharacter.IsDead || targetCharacter.Removed || !(targetCharacter.Stun > 0) && !targetCharacter.IsUnconscious && !targetCharacter.LockHands) { return; } + + if (GameMain.Server.TraitorManager?.Traitors != null) + { + if (GameMain.Server.TraitorManager.Traitors.Any(t => t.Character == targetCharacter || t.Character == yoinkerCharacter)) + { + // Don't penalize traitors + return; + } + } + + var foundItem = Inventory.FindItemRecursive(item, it => it.Prefab.Identifier == "idcard" || it.GetComponent() != null || it.GetComponent() != null); + + if (foundItem == null) { return; } + + bool isIdCard = foundItem.prefab.Identifier == "idcard"; + bool isWeapon = foundItem.GetComponent() != null || foundItem.GetComponent() != null; + + if (isIdCard) + { + string name = string.Empty; + + foreach (var tag in foundItem.Tags.Split(',')) + { + string[] split = tag.Split(':'); + string key = split.Length > 0 ? split[0] : string.Empty; + string value = split.Length > 1 ? split[1] : string.Empty; + if (key == "name") { name = value; } + } + + // Name tag doesn't belong to anyone in particular or we own the ID card + if (name == null || name == yoinkerCharacter.Name) { return; } + } + + if (MathUtils.NearlyEqual(DangerousItemStealKarmaDecrease, 0)) { return; } + + const float calcUpper = 1, calcLower = -1; + + float upper = DangerousItemStealKarmaDecrease + 10.0f; + float lower = DangerousItemStealKarmaDecrease - 10.0f; + + if (lower < 0) + { + upper += Math.Abs(lower); + lower = 0; + } + + // If we're stealing from a bot assume the bot has 50 karma + var targetKarma = targetClient?.Karma ?? 50; + + float karmaDifference = Math.Clamp((targetKarma - yoinker.Karma) / 50.0f, calcLower, calcUpper); + float karmaDecrease = lower + (karmaDifference - calcLower) * (upper - lower) / (calcUpper - calcLower); + + JobPrefab clientJob = yoinker.CharacterInfo?.Job?.Prefab; + + // security officers receive less karma penalty + if (clientJob != null && clientJob.Identifier == "securityofficer" && isWeapon) + { + karmaDecrease *= 0.5f; + } + + AdjustKarma(yoinkerCharacter, -karmaDecrease, "Stolen dangerous item"); + } + + public void OnCharacterHealthChanged(Character target, Character attacker, float damage, float stun, IEnumerable appliedAfflictions = null) { if (target == null || attacker == null) { return; } if (target == attacker) { return; } @@ -238,11 +336,11 @@ namespace Barotrauma if (appliedAfflictions != null) { - foreach (Affliction affliction in appliedAfflictions) - { - if (MathUtils.NearlyEqual(affliction.Prefab.KarmaChangeOnApplied, 0.0f)) { continue; } - damage -= affliction.Prefab.KarmaChangeOnApplied * affliction.Strength; - } + foreach (Affliction affliction in appliedAfflictions) + { + if (MathUtils.NearlyEqual(affliction.Prefab.KarmaChangeOnApplied, 0.0f)) { continue; } + damage -= affliction.Prefab.KarmaChangeOnApplied * affliction.Strength; + } } Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == target); @@ -253,15 +351,16 @@ namespace Barotrauma } Client attackerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == attacker); - if (attackerClient != null) + ClientMemory attackerMemory = GetClientMemory(attackerClient); + if (attackerMemory != null) { //if the attacker has been attacked by the target within the last x seconds, ignore the damage //(= no karma penalty from retaliating against someone who attacked you) - var attackerMemory = GetClientMemory(attackerClient); if (attackerMemory.LastAttackTime.ContainsKey(target) && attackerMemory.LastAttackTime[target] > Timing.TotalTime - AllowedRetaliationTime) { damage = Math.Min(damage, 0); + stun = 0.0f; } } @@ -270,6 +369,7 @@ namespace Barotrauma target.HasEquippedItem("clowncostume")) { damage *= 0.5f; + stun *= 0.5f; } //smaller karma penalty for attacking someone who's aiming with a weapon @@ -278,6 +378,7 @@ namespace Barotrauma target.SelectedItems.Any(it => it != null && (it.GetComponent() != null || it.GetComponent() != null))) { damage *= 0.5f; + stun *= 0.5f; } //damage scales according to the karma of the target @@ -298,6 +399,30 @@ namespace Barotrauma } else { + if (stun > 0 && attackerMemory != null) + { + //GameServer.Log(GameServer.CharacterLogName(attacker) + " stunned " + GameServer.CharacterLogName(target) + $" ({stun})", ServerLog.MessageType.Karma); + attackerMemory.StunsInPastMinute.Add(new ClientMemory.TimeAmount() { Time = Timing.TotalTime, Amount = stun }); + + if (attackerMemory.StunsInPastMinute.Count > 1) + { + float avgStunsInflicted = attackerMemory.StunsInPastMinute[0].Amount / (float)(attackerMemory.StunsInPastMinute[1].Time - attackerMemory.StunsInPastMinute[0].Time); + for (int i = 1; i < attackerMemory.StunsInPastMinute.Count; i++) + { + avgStunsInflicted += attackerMemory.StunsInPastMinute[i].Amount / (float)(attackerMemory.StunsInPastMinute[i].Time - attackerMemory.StunsInPastMinute[i - 1].Time); + } + + //GameServer.Log(avgStunsInflicted.ToString(), ServerLog.MessageType.Karma); + + if (avgStunsInflicted > StunFriendlyKarmaDecreaseThreshold || + attackerMemory.StunKarmaDecreaseMultiplier > 1.0f) + { + AdjustKarma(attacker, -StunFriendlyKarmaDecrease * attackerMemory.StunKarmaDecreaseMultiplier, "Stunned friendly"); + attackerMemory.StunKarmaDecreaseMultiplier *= 2.0f; + } + } + } + if (damage > 0) { AdjustKarma(attacker, -damage * DamageFriendlyKarmaDecrease, "Damaged friendly"); @@ -395,6 +520,7 @@ namespace Barotrauma private ClientMemory GetClientMemory(Client client) { + if (client == null) { return null; } if (!clientMemories.ContainsKey(client)) { clientMemories[client] = new ClientMemory() @@ -429,6 +555,21 @@ namespace Barotrauma } client.Karma += amount; + + if (amount < 0.0f) + { + float? herpesStrength = client.Character?.CharacterHealth.GetAfflictionStrength("spaceherpes"); + var clientMemory = GetClientMemory(client); + clientMemory.KarmaDecreasesInPastMinute.RemoveAll(ta => ta.Time + 60.0f < Timing.TotalTime); + float aggregate = clientMemory.KarmaDecreasesInPastMinute.Select(ta => ta.Amount).DefaultIfEmpty().Aggregate((a, b) => a + b); + clientMemory.KarmaDecreasesInPastMinute.Add(new ClientMemory.TimeAmount() { Time = Timing.TotalTime, Amount = -amount }); + + if (herpesStrength.HasValue && herpesStrength <= 0.0f && aggregate - amount > 25.0f && aggregate <= 25.0f) + { + GameServer.Log($"{GameServer.ClientLogName(client)} has lost more than 25 karma in the past minute.", ServerLog.MessageType.Karma); + } + } + if (TestMode) { SendKarmaNotifications(client, debugKarmaChangeReason); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs index 81c37c14c..7dfb9041a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs @@ -138,7 +138,12 @@ namespace Barotrauma.Networking //remove old events that have been sent to all clients, they are redundant now // keep at least one event in the list (lastSentToAll == e.ID) so we can use it to keep track of the latest ID // and events less than 15 seconds old to give disconnected clients a bit of time to reconnect without getting desynced - events.RemoveAll(e => (NetIdUtils.IdMoreRecent(lastSentToAll, e.ID) || !inGameClientsPresent) && e.CreateTime < Timing.TotalTime - 15.0f); + if (Timing.TotalTime > GameMain.GameSession.RoundStartTime + NetConfig.RoundStartSyncDuration) + { + events.RemoveAll(e => + (NetIdUtils.IdMoreRecent(lastSentToAll, e.ID) || !inGameClientsPresent) && + e.CreateTime < Timing.TotalTime - NetConfig.EventRemovalTime); + } for (int i = events.Count - 1; i >= 0; i--) { @@ -224,7 +229,9 @@ namespace Barotrauma.Networking }); lastSentToAnyoneTime = events.Find(e => e.ID == lastSentToAnyone)?.CreateTime ?? Timing.TotalTime; - if ((Timing.TotalTime - lastSentToAnyoneTime) > 10.0 && (Timing.TotalTime - lastWarningTime) > 5.0) + if (Timing.TotalTime - lastWarningTime > 5.0 && + Timing.TotalTime - lastSentToAnyoneTime > 10.0 && + Timing.TotalTime > GameMain.GameSession.RoundStartTime + NetConfig.RoundStartSyncDuration) { lastWarningTime = Timing.TotalTime; GameServer.Log("WARNING: ServerEntityEventManager is lagging behind! Last sent id: " + lastSentToAnyone.ToString() + ", latest create id: " + ID.ToString(), ServerLog.MessageType.ServerMessage); @@ -235,19 +242,21 @@ namespace Barotrauma.Networking clients.Where(c => c.NeedsMidRoundSync).ForEach(c => { if (NetIdUtils.IdMoreRecent(lastSentToAll, c.FirstNewEventID)) lastSentToAll = (ushort)(c.FirstNewEventID - 1); }); ServerEntityEvent firstEventToResend = events.Find(e => e.ID == (ushort)(lastSentToAll + 1)); - if (firstEventToResend != null && ((lastSentToAnyoneTime - firstEventToResend.CreateTime) > 10.0 || (Timing.TotalTime - firstEventToResend.CreateTime) > 30.0)) + if (firstEventToResend != null && + Timing.TotalTime > GameMain.GameSession.RoundStartTime + NetConfig.RoundStartSyncDuration && + ((lastSentToAnyoneTime - firstEventToResend.CreateTime) > NetConfig.OldReceivedEventKickTime || (Timing.TotalTime - firstEventToResend.CreateTime) > NetConfig.OldEventKickTime)) { // This event is 10 seconds older than the last one we've successfully sent, // kick everyone that hasn't received it yet, this is way too old // UNLESS the event was created when the client was still midround syncing, // in which case we'll wait until the timeout runs out before kicking the client List toKick = inGameClients.FindAll(c => - NetIdUtils.IdMoreRecent((UInt16)(lastSentToAll + 1), c.LastRecvEntityEventID) && + NetIdUtils.IdMoreRecent((UInt16)(lastSentToAll + 1), c.LastRecvEntityEventID) && (firstEventToResend.CreateTime > c.MidRoundSyncTimeOut || lastSentToAnyoneTime > c.MidRoundSyncTimeOut || Timing.TotalTime > c.MidRoundSyncTimeOut + 10.0)); toKick.ForEach(c => { DebugConsole.NewMessage(c.Name + " was kicked due to excessive desync (expected old event " + (c.LastRecvEntityEventID + 1).ToString() + ")", Color.Red); - GameServer.Log("Disconnecting client " + c.Name + " due to excessive desync (expected old event " + GameServer.Log("Disconnecting client " + GameServer.ClientLogName(c) + " due to excessive desync (expected old event " + (c.LastRecvEntityEventID + 1).ToString() + " (created " + (Timing.TotalTime - firstEventToResend.CreateTime).ToString("0.##") + " s ago, " + (lastSentToAnyoneTime - firstEventToResend.CreateTime).ToString("0.##") + " s older than last event sent to anyone)" + @@ -265,7 +274,7 @@ namespace Barotrauma.Networking toKick.ForEach(c => { DebugConsole.NewMessage(c.Name + " was kicked due to excessive desync (expected removed event " + (c.LastRecvEntityEventID + 1).ToString() + ", last available is " + events[0].ID.ToString() + ")", Color.Red); - GameServer.Log("Disconnecting client " + c.Name + " due to excessive desync (expected removed event " + (c.LastRecvEntityEventID + 1).ToString() + ", last available is " + events[0].ID.ToString() + ")", ServerLog.MessageType.Error); + GameServer.Log("Disconnecting client " + GameServer.ClientLogName(c) + " due to excessive desync (expected removed event " + (c.LastRecvEntityEventID + 1).ToString() + ", last available is " + events[0].ID.ToString() + ")", ServerLog.MessageType.Error); server.DisconnectClient(c, "", DisconnectReason.ExcessiveDesyncRemovedEvent + "/ServerMessage.ExcessiveDesyncRemovedEvent"); }); } @@ -274,7 +283,7 @@ namespace Barotrauma.Networking var timedOutClients = clients.FindAll(c => c.Connection != GameMain.Server.OwnerConnection && c.InGame && c.NeedsMidRoundSync && Timing.TotalTime > c.MidRoundSyncTimeOut); foreach (Client timedOutClient in timedOutClients) { - GameServer.Log("Disconnecting client " + timedOutClient.Name + ". Syncing the client with the server took too long.", ServerLog.MessageType.Error); + GameServer.Log("Disconnecting client " + GameServer.ClientLogName(timedOutClient) + ". Syncing the client with the server took too long.", ServerLog.MessageType.Error); GameMain.Server.DisconnectClient(timedOutClient, "", DisconnectReason.SyncTimeout + "/ServerMessage.SyncTimeout"); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs index f64f8c05a..1f74943b3 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs @@ -344,13 +344,21 @@ namespace Barotrauma.Networking if (!isCompatibleVersion) { RemovePendingClient(pendingClient, DisconnectReason.InvalidVersion, - $"DisconnectMessage.InvalidVersion~[version]={GameMain.Version.ToString()}~[clientversion]={version}"); + $"DisconnectMessage.InvalidVersion~[version]={GameMain.Version}~[clientversion]={version}"); GameServer.Log(name + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (incompatible game version)", ServerLog.MessageType.Error); DebugConsole.NewMessage(name + " (" + inc.SenderConnection.RemoteEndPoint.Address.ToString() + ") couldn't join the server (incompatible game version)", Microsoft.Xna.Framework.Color.Red); return; } + Client nameTaken = GameMain.Server.ConnectedClients.Find(c => Homoglyphs.Compare(c.Name.ToLower(), name.ToLower())); + if (nameTaken != null) + { + RemovePendingClient(pendingClient, DisconnectReason.NameTaken, ""); + GameServer.Log(name + " (" + inc.SenderConnection.RemoteEndPoint.Address + ") couldn't join the server (name too similar to the name of the client \"" + nameTaken.Name + "\").", ServerLog.MessageType.Error); + return; + } + int contentPackageCount = inc.ReadVariableInt32(); List clientContentPackages = new List(); for (int i = 0; i < contentPackageCount; i++) @@ -419,12 +427,12 @@ namespace Barotrauma.Networking //steam auth cannot be done (SteamManager not initialized or no ticket given), //but it's not required either -> let the client join without auth - if ((!Steam.SteamManager.IsInitialized || (ticket?.Length??0) == 0) && + if ((!Steam.SteamManager.IsInitialized || (ticket?.Length ?? 0) == 0) && !requireSteamAuth) { pendingClient.Name = name; pendingClient.OwnerKey = ownKey; - pendingClient.InitializationStep = ConnectionInitialization.ContentPackageOrder; + pendingClient.InitializationStep = serverSettings.HasPassword ? ConnectionInitialization.Password : ConnectionInitialization.ContentPackageOrder; } else { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs index 41b828eff..62898a9c2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/SteamP2PServerPeer.cs @@ -306,13 +306,21 @@ namespace Barotrauma.Networking if (!isCompatibleVersion) { RemovePendingClient(pendingClient, DisconnectReason.InvalidVersion, - $"DisconnectMessage.InvalidVersion~[version]={GameMain.Version.ToString()}~[clientversion]={version}"); + $"DisconnectMessage.InvalidVersion~[version]={GameMain.Version}~[clientversion]={version}"); GameServer.Log(name + " (" + pendingClient.SteamID.ToString() + ") couldn't join the server (incompatible game version)", ServerLog.MessageType.Error); DebugConsole.NewMessage(name + " (" + pendingClient.SteamID.ToString() + ") couldn't join the server (incompatible game version)", Microsoft.Xna.Framework.Color.Red); return; } + Client nameTaken = GameMain.Server.ConnectedClients.Find(c => Homoglyphs.Compare(c.Name.ToLower(), name.ToLower())); + if (nameTaken != null) + { + RemovePendingClient(pendingClient, DisconnectReason.NameTaken, ""); + GameServer.Log(name + " (" + pendingClient.SteamID.ToString() + ") couldn't join the server (name too similar to the name of the client \"" + nameTaken.Name + "\").", ServerLog.MessageType.Error); + return; + } + int contentPackageCount = (int)inc.ReadVariableUInt32(); List clientContentPackages = new List(); for (int i = 0; i < contentPackageCount; i++) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index 8a12853f7..f86579619 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -282,7 +282,7 @@ namespace Barotrauma.Networking clients[i].Character = character; character.OwnerClientEndPoint = clients[i].Connection.EndPointString; character.OwnerClientName = clients[i].Name; - GameServer.Log(string.Format("Respawning {0} ({1}) as {2}", clients[i].Name, clients[i].Connection?.EndPointString, characterInfos[i].Job.Name), ServerLog.MessageType.Spawning); + GameServer.Log(string.Format("Respawning {0} ({1}) as {2}", GameServer.ClientLogName(clients[i]), clients[i].Connection?.EndPointString, characterInfos[i].Job.Name), ServerLog.MessageType.Spawning); } if (divingSuitPrefab != null && oxyPrefab != null && RespawnShuttle != null) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index b92cc8ea1..1493e987b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -1,10 +1,9 @@ -using Microsoft.Xna.Framework; +using Barotrauma.IO; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Globalization; -using System.IO; using System.Linq; -using System.Xml; using System.Xml.Linq; namespace Barotrauma.Networking @@ -108,7 +107,7 @@ namespace Barotrauma.Networking netProperties[key].Read(incMsg); if (!netProperties[key].PropEquals(prevValue, netProperties[key])) { - GameServer.Log(c.Name + " changed " + netProperties[key].Name + " to " + netProperties[key].Value.ToString(), ServerLog.MessageType.ServerMessage); + GameServer.Log(GameServer.ClientLogName(c) + " changed " + netProperties[key].Name + " to " + netProperties[key].Value.ToString(), ServerLog.MessageType.ServerMessage); } changed = true; } @@ -204,7 +203,7 @@ namespace Barotrauma.Networking SerializableProperty.SerializeProperties(this, doc.Root, true); - XmlWriterSettings settings = new XmlWriterSettings + System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { Indent = true, NewLineOnAttributes = true @@ -212,7 +211,7 @@ namespace Barotrauma.Networking using (var writer = XmlWriter.Create(SettingsFile, settings)) { - doc.Save(writer); + doc.SaveSafe(writer); } if (KarmaPreset == "custom") @@ -521,13 +520,13 @@ namespace Barotrauma.Networking try { - XmlWriterSettings settings = new XmlWriterSettings(); + System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings(); settings.Indent = true; settings.NewLineOnAttributes = true; using (var writer = XmlWriter.Create(ClientPermissionsFile, settings)) { - doc.Save(writer); + doc.SaveSafe(writer); } } catch (Exception e) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs index b923dc1d5..4fcdc6f73 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs @@ -89,7 +89,8 @@ namespace Barotrauma.Networking if (sender.Character != null && sender.Character.SpeechImpediment >= 100.0f) { return false; } //check if the message can be sent via radio - if (ChatMessage.CanUseRadio(sender.Character, out WifiComponent senderRadio) && + if (!sender.VoipQueue.ForceLocal && + ChatMessage.CanUseRadio(sender.Character, out WifiComponent senderRadio) && ChatMessage.CanUseRadio(recipient.Character, out WifiComponent recipientRadio)) { if (recipientRadio.CanReceive(senderRadio)) { return true; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs index 9052887a6..b04f53d5d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs @@ -74,7 +74,7 @@ namespace Barotrauma if (ready != sender.GetVote(VoteType.StartRound)) { sender.SetVote(VoteType.StartRound, ready); - GameServer.Log(sender.Name + (ready ? " is ready to start the game." : " is not ready to start the game."), ServerLog.MessageType.ServerMessage); + GameServer.Log(GameServer.ClientLogName(sender) + (ready ? " is ready to start the game." : " is not ready to start the game."), ServerLog.MessageType.ServerMessage); } break; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/WhiteList.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/WhiteList.cs index c50de1630..aaaa0894f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/WhiteList.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/WhiteList.cs @@ -1,7 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Net; @@ -181,7 +181,7 @@ namespace Barotrauma.Networking WhiteListedPlayer whitelistedPlayer = whitelistedPlayers.Find(p => p.UniqueIdentifier == id); if (whitelistedPlayer != null) { - GameServer.Log(c.Name + " removed " + whitelistedPlayer.Name + " from whitelist (" + whitelistedPlayer.IP + ")", ServerLog.MessageType.ConsoleUsage); + GameServer.Log(GameServer.ClientLogName(c) + " removed " + whitelistedPlayer.Name + " from whitelist (" + whitelistedPlayer.IP + ")", ServerLog.MessageType.ConsoleUsage); RemoveFromWhiteList(whitelistedPlayer); } } @@ -192,7 +192,7 @@ namespace Barotrauma.Networking string name = incMsg.ReadString(); string ip = incMsg.ReadString(); - GameServer.Log(c.Name + " added " + name + " to whitelist (" + ip + ")", ServerLog.MessageType.ConsoleUsage); + GameServer.Log(GameServer.ClientLogName(c) + " added " + name + " to whitelist (" + ip + ")", ServerLog.MessageType.ConsoleUsage); AddToWhiteList(name, ip); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Program.cs b/Barotrauma/BarotraumaServer/ServerSource/Program.cs index e355d1d3d..d2aa25c0e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Program.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Program.cs @@ -3,7 +3,7 @@ using Barotrauma.Steam; using GameAnalyticsSDK.Net; using System; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Text; using System.Threading; @@ -85,14 +85,16 @@ namespace Barotrauma filePath = Path.GetFileNameWithoutExtension(originalFilePath) + " (" + (existingFiles + 1) + ")" + Path.GetExtension(originalFilePath); } - StreamWriter sw = new StreamWriter(filePath); - StringBuilder sb = new StringBuilder(); sb.AppendLine("Barotrauma Dedicated Server crash report (generated on " + DateTime.Now + ")"); sb.AppendLine("\n"); sb.AppendLine("Barotrauma seems to have crashed. Sorry for the inconvenience! "); sb.AppendLine("\n"); sb.AppendLine("Game version " + GameMain.Version + " (" + AssemblyInfo.GetBuildString() + ", branch " + AssemblyInfo.GetGitBranch() + ", revision " + AssemblyInfo.GetGitRevision() + ")"); + if (GameMain.Config != null) + { + sb.AppendLine("Language: " + (GameMain.Config.Language ?? "none")); + } if (GameMain.SelectedPackages != null) { sb.AppendLine("Selected content packages: " + (!GameMain.SelectedPackages.Any() ? "None" : string.Join(", ", GameMain.SelectedPackages.Select(c => c.Name)))); @@ -138,8 +140,7 @@ namespace Barotrauma Console.ForegroundColor = ConsoleColor.Red; Console.Write(crashReport); - sw.WriteLine(sb.ToString()); - sw.Close(); + File.WriteAllText(filePath,sb.ToString()); if (GameSettings.SendUserStatistics) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalDestroyItemsWithTag.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalDestroyItemsWithTag.cs index 551158b07..17e4ae552 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalDestroyItemsWithTag.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalDestroyItemsWithTag.cs @@ -38,7 +38,9 @@ namespace Barotrauma if (item.Submarine == null) { - if (!(item.ParentInventory?.Owner is Character)) { continue; } + //items outside the sub don't count as destroyed if they're still in the traitor's inventory + bool carriedByTraitor = Traitors.Any(traitor => item.IsOwnedBy(traitor.Character)); + if (!carriedByTraitor) { continue; } } else { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs index 730cadcde..d51c0f9da 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs @@ -5,7 +5,7 @@ using System; using Barotrauma.Networking; using Lidgren.Network; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Security.Cryptography; using Barotrauma.Extensions; @@ -235,7 +235,7 @@ namespace Barotrauma #if SERVER foreach (var traitor in Traitors.Values) { - GameServer.Log($"{traitor.Character.Name} is a traitor and the current goals are:\n{(traitor.CurrentObjective?.GoalInfos != null ? TextManager.GetServerMessage(traitor.CurrentObjective?.GoalInfos) : "(empty)")}", ServerLog.MessageType.ServerMessage); + GameServer.Log($"{GameServer.CharacterLogName(traitor.Character)} is a traitor and the current goals are:\n{(traitor.CurrentObjective?.GoalInfos != null ? TextManager.GetServerMessage(traitor.CurrentObjective?.GoalInfos) : "(empty)")}", ServerLog.MessageType.ServerMessage); } #endif return true; @@ -287,10 +287,7 @@ namespace Barotrauma { pendingObjectives.Remove(objective); completedObjectives.Add(objective); - if (pendingObjectives.Count > 0) - { - objective.EndMessage(); - } + objective.EndMessage(); continue; } if (objective.IsStarted && !objective.CanBeCompleted) diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 978f3cde6..38d0a73c6 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.9.9.0 + 0.9.1000.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml index 62415487a..454583cdf 100644 --- a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml +++ b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml @@ -53,6 +53,7 @@ + @@ -75,6 +76,8 @@ + + diff --git a/Barotrauma/BarotraumaShared/Data/karmasettings.xml b/Barotrauma/BarotraumaShared/Data/karmasettings.xml index a29ef9dd9..f358b0b39 100644 --- a/Barotrauma/BarotraumaShared/Data/karmasettings.xml +++ b/Barotrauma/BarotraumaShared/Data/karmasettings.xml @@ -22,7 +22,9 @@ kickbanthreshold="1" kicksbeforeban="3" karmanotificationinterval="15" - resetkarmabetweenrounds="true" /> + resetkarmabetweenrounds="true" + dangerousitemstealkarmadecrease="15" + dangerousitemstealbots="false" /> + resetkarmabetweenrounds="true" + dangerousitemstealkarmadecrease="15" + dangerousitemstealbots="true" /> + resetkarmabetweenrounds="true" + dangerousitemstealkarmadecrease="15" + dangerousitemstealbots="false" /> \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs index c60855d1a..b0bdc41e9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AIController.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; namespace Barotrauma { - public enum AIState { Idle, Attack, Escape, Eat, Flee, Avoid, Aggressive, PassiveAggressive } + public enum AIState { Idle, Attack, Escape, Eat, Flee, Avoid, Aggressive, PassiveAggressive, Protect } abstract partial class AIController : ISteerable { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 11e3fdebe..0940a9091 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -36,14 +36,17 @@ namespace Barotrauma private readonly float updateTargetsInterval = 1; private readonly float updateMemoriesInverval = 1; + private readonly float attackLimbResetInterval = 2; - private float avoidLookAheadDistance; + private readonly float avoidLookAheadDistance; + private IndoorsSteeringManager PathSteering => insideSteering as IndoorsSteeringManager; private SteeringManager outsideSteering, insideSteering; private float updateTargetsTimer; private float updateMemoriesTimer; - + private float attackLimbResetTimer; + private bool IsCoolDownRunning => AttackingLimb != null && AttackingLimb.attack.CoolDownTimer > 0; public float CombatStrength => Character.Params.AI.CombatStrength; private float Sight => Character.Params.AI.Sight; @@ -63,6 +66,7 @@ namespace Barotrauma get { return _attackingLimb; } private set { + attackLimbResetTimer = 0; _attackingLimb = value; attackVector = null; Reverse = _attackingLimb != null && _attackingLimb.attack.Reverse; @@ -82,8 +86,9 @@ namespace Barotrauma private readonly float colliderWidth; private readonly float colliderLength; private readonly int requiredHoleCount; - private readonly bool canAttackSub; - private readonly bool canAttackCharacters; + private bool canAttackWalls; + private bool canAttackDoors; + private bool canAttackCharacters; private readonly float priorityFearIncreasement = 2; private readonly float memoryFadeTime = 0.5f; @@ -183,24 +188,9 @@ namespace Barotrauma } } - bool canBreakDoors = false; - if (GetTarget("room")?.Priority > 0.0f) - { - 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(currentContexts) && limb.attack.StructureDamage > 0.0f) - { - canBreakDoors = true; - break; - } - } - } - + ReevaluateAttacks(); outsideSteering = new SteeringManager(this); - insideSteering = new IndoorsSteeringManager(this, false, canBreakDoors); + insideSteering = new IndoorsSteeringManager(this, false, canAttackDoors); steeringManager = outsideSteering; State = AIState.Idle; @@ -210,9 +200,6 @@ namespace Barotrauma requiredHoleCount = (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderWidth) / Structure.WallSectionSize); avoidLookAheadDistance = Math.Max(colliderWidth * 3, 1.5f); - - canAttackSub = Character.AnimController.CanAttackSubmarine; - canAttackCharacters = Character.AnimController.CanAttackCharacters; } private CharacterParams.AIParams AIParams => Character.Params.AI; @@ -227,27 +214,28 @@ namespace Barotrauma selectedTargetMemory.Priority = priority; } - private float escapeMargin; + private float movementMargin; 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) + bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X)); + if (steeringManager == insideSteering) { - var currPath = ((IndoorsSteeringManager)steeringManager).CurrentPath; + var currPath = PathSteering.CurrentPath; if (currPath != null && currPath.CurrentNode != null) { if (currPath.CurrentNode.SimPosition.Y < Character.AnimController.GetColliderBottom().Y) { - ignorePlatforms = true; + // Don't allow to jump from too high. + float allowedJumpHeight = Character.AnimController.ImpactTolerance / 2; + float height = Math.Abs(currPath.CurrentNode.SimPosition.Y - Character.SimPosition.Y); + ignorePlatforms = height < allowedJumpHeight; } } } - Character.AnimController.IgnorePlatforms = ignorePlatforms; //clients get the facing direction from the server @@ -376,7 +364,7 @@ namespace Barotrauma return; } float squaredDistance = Vector2.DistanceSquared(WorldPosition, SelectedAiTarget.WorldPosition); - var attackLimb = GetAttackLimb(SelectedAiTarget.WorldPosition); + var attackLimb = AttackingLimb ?? GetAttackLimb(SelectedAiTarget.WorldPosition); if (attackLimb != null && squaredDistance <= Math.Pow(attackLimb.attack.Range, 2)) { run = true; @@ -393,7 +381,7 @@ namespace Barotrauma { bool isBeingChased = IsBeingChased; float reactDistance = !isBeingChased && selectedTargetingParams != null && selectedTargetingParams.ReactDistance > 0 ? selectedTargetingParams.ReactDistance : GetPerceivingRange(SelectedAiTarget); - if (squaredDistance <= Math.Pow(reactDistance + escapeMargin, 2)) + if (squaredDistance <= Math.Pow(reactDistance + movementMargin, 2)) { float halfReactDistance = reactDistance / 2; float attackDistance = selectedTargetingParams != null && selectedTargetingParams.AttackDistance > 0 ? selectedTargetingParams.AttackDistance : halfReactDistance; @@ -405,21 +393,48 @@ namespace Barotrauma else { run = isBeingChased ? true : squaredDistance < Math.Pow(halfReactDistance, 2); - if (escapeMargin <= 0) + if (movementMargin <= 0) { - escapeMargin = halfReactDistance; + movementMargin = halfReactDistance; } - escapeMargin = MathHelper.Clamp(escapeMargin += deltaTime, halfReactDistance, reactDistance); + movementMargin = MathHelper.Clamp(movementMargin += deltaTime, halfReactDistance, reactDistance); UpdateEscape(deltaTime); } } else { - escapeMargin = 0; + movementMargin = 0; UpdateIdle(deltaTime); } } break; + case AIState.Protect: + if (SelectedAiTarget == null || SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed) + { + State = AIState.Idle; + return; + } + if (SelectedAiTarget.Entity is Character targetCharacter && targetCharacter.LastAttacker is Character attacker) + { + // Attack the character that attacked the target we are protecting + ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2); + SelectTarget(attacker.AiTarget); + return; + } + float sqrDist = Vector2.DistanceSquared(WorldPosition, SelectedAiTarget.WorldPosition); + float reactDist = selectedTargetingParams != null && selectedTargetingParams.ReactDistance > 0 ? selectedTargetingParams.ReactDistance : GetPerceivingRange(SelectedAiTarget); + if (sqrDist > Math.Pow(reactDist + movementMargin, 2)) + { + movementMargin = reactDist; + run = true; + UpdateFollow(deltaTime); + } + else + { + movementMargin = MathHelper.Clamp(movementMargin -= deltaTime, 0, reactDist); + UpdateIdle(deltaTime); + } + break; default: throw new NotImplementedException(); } @@ -432,6 +447,8 @@ namespace Barotrauma SwarmBehavior.Refresh(); SwarmBehavior.UpdateSteering(deltaTime); } + // Ensure that the creature keeps inside the level + SteerInsideLevel(deltaTime); float speed = Character.AnimController.GetCurrentSpeed(run && Character.CanRun); steeringManager.Update(speed); Character.AnimController.TargetMovement = Character.ApplyMovementLimits(Steering, State == AIState.Idle && Character.AnimController.InWater ? Steering.Length() : speed); @@ -456,7 +473,6 @@ namespace Barotrauma SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5); return; } - SteerInsideLevel(deltaTime); } var target = SelectedAiTarget ?? _lastAiTarget; if (target?.Entity != null && !target.Entity.Removed && PreviousState == AIState.Attack && Character.CurrentHull == null) @@ -576,10 +592,10 @@ namespace Barotrauma } else if (pathSteering != null) { - if (canAttackSub && hasValidPath) + if (canAttackDoors && hasValidPath) { var door = pathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? pathSteering.CurrentPath.NextNode?.ConnectedDoor; - if (door != null && !door.IsOpen) + if (door != null && !door.IsOpen && !door.IsBroken) { if (SelectedAiTarget != door.Item.AiTarget) { @@ -615,7 +631,6 @@ namespace Barotrauma { SteeringManager.SteeringWander(); SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 5); - SteerInsideLevel(deltaTime); } } } @@ -641,13 +656,18 @@ namespace Barotrauma if (SelectedAiTarget.Entity is Item item) { // If the item is held by a character, attack the character instead. - var pickable = item.GetComponent(); - if (pickable != null) + Character owner = GetOwner(item); + if (owner != null) { - var target = pickable.Picker?.AiTarget; - if (target?.Entity != null && !target.Entity.Removed) + if (IsFriendly(Character, owner)) { - SelectedAiTarget = target; + ResetAITarget(); + State = AIState.Idle; + return; + } + else + { + SelectedAiTarget = owner.AiTarget; } } } @@ -659,7 +679,8 @@ namespace Barotrauma { attackWorldPos += wallTarget.Structure.Submarine.Position; } - attackSimPos = ConvertUnits.ToSimUnits(attackWorldPos); + attackSimPos = Character.Submarine == wallTarget.Structure.Submarine ? wallTarget.Position : attackWorldPos; + attackSimPos = ConvertUnits.ToSimUnits(attackSimPos); } else { @@ -685,7 +706,7 @@ namespace Barotrauma WallSection section = wall.Sections[i]; if (CanPassThroughHole(wall, i) && section?.gap != null) { - if (SteerThroughGap(wall, section, section.gap.WorldPosition, deltaTime)) + if (SteerThroughGap(wall, section, wall.SectionPosition(i, true), deltaTime)) { return; } @@ -697,7 +718,7 @@ namespace Barotrauma var door = i.GetComponent(); // 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) + if (door?.LinkedGap?.FlowTargetHull != null && !door.LinkedGap.IsRoomToRoom && (door.IsOpen || door.IsBroken)) { if (Character.AnimController.CanWalk || door.LinkedGap.FlowTargetHull.WaterPercentage > 25) { @@ -715,7 +736,6 @@ namespace Barotrauma } 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++) { @@ -886,24 +906,11 @@ namespace Barotrauma } canAttack = AttackingLimb != null && AttackingLimb.attack.CoolDownTimer <= 0; } - if (!canAttack && SelectedAiTarget.Entity.Submarine != null && !canAttackSub) + if (!Character.AnimController.SimplePhysicsEnabled && SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null && (!canAttackDoors || !canAttackWalls || !AIParams.TargetOuterWalls)) { - float dist = Vector2.Distance(Character.AnimController.MainLimb.WorldPosition, attackWorldPos); - if (wallTarget != null) + if (Vector2.Distance(Character.WorldPosition, attackWorldPos) < 2000 * 2000) { - // Steer towards the target, but turn away if a wall is blocking the way - if (dist < ConvertUnits.ToDisplayUnits(colliderLength) * 3) - { - State = AIState.Idle; - IgnoreTarget(SelectedAiTarget); - // Resetting the ai target prevents the character from chasing it - ResetAITarget(); - return; - } - } - else if (dist < 1000) - { - // Check that we are not bumping into a door + // Check that we are not bumping into a door or a wall Vector2 rayStart = SimPosition; if (Character.Submarine == null) { @@ -912,9 +919,10 @@ namespace Barotrauma Vector2 toTarget = SelectedAiTarget.WorldPosition - WorldPosition; Vector2 rayEnd = rayStart + toTarget.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 2); Body closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true); - if (Submarine.LastPickedFraction != 1.0f && closestBody != null && closestBody.UserData is Item i && i.Submarine != null && i.GetComponent() != null) + if (Submarine.LastPickedFraction != 1.0f && closestBody != null && + (!AIParams.TargetOuterWalls || !canAttackWalls && closestBody.UserData is Structure s && s.Submarine != null || !canAttackDoors && closestBody.UserData is Item i && i.Submarine != null && i.GetComponent() != null)) { - // Target is unreachable, there's a door ahead + // Target is unreachable, there's a door or wall ahead State = AIState.Idle; IgnoreTarget(SelectedAiTarget); ResetAITarget(); @@ -925,7 +933,7 @@ namespace Barotrauma float distance = 0; Limb attackTargetLimb = null; Character targetCharacter = SelectedAiTarget.Entity as Character; - if (canAttack) + if (canAttack && !Character.AnimController.SimplePhysicsEnabled) { // Target a specific limb instead of the target center position if (wallTarget == null && targetCharacter != null) @@ -943,7 +951,7 @@ namespace Barotrauma attackSimPos = Character.GetRelativeSimPosition(attackTargetLimb); } // Check that we can reach the target - Vector2 toTarget = attackWorldPos - AttackingLimb.WorldPosition; + Vector2 toTarget = attackWorldPos - (Character.AnimController.SimplePhysicsEnabled ? Character.WorldPosition : AttackingLimb.WorldPosition); if (wallTarget != null) { if (wallTarget.Structure.Submarine != null) @@ -980,7 +988,15 @@ namespace Barotrauma { // If not, reset the attacking limb, if the cooldown is not running // Don't use the property, because we don't want cancel reversing, if we are reversing. - _attackingLimb = null; + if (attackLimbResetTimer > attackLimbResetInterval) + { + _attackingLimb = null; + attackLimbResetTimer = 0; + } + else + { + attackLimbResetTimer += deltaTime; + } } } Limb steeringLimb = canAttack ? AttackingLimb : null; @@ -1020,21 +1036,25 @@ namespace Barotrauma } else { - Vector2 offset = Character.SimPosition - steeringLimb.SimPosition; - // Offset so that we don't overshoot the movement - Vector2 steerPos = attackSimPos + offset; + Vector2 steerPos = attackSimPos; + if (!Character.AnimController.SimplePhysicsEnabled) + { + // Offset so that we don't overshoot the movement + Vector2 offset = Character.SimPosition - steeringLimb.SimPosition; + steerPos += offset; + } if (SteeringManager is IndoorsSteeringManager pathSteering) { if (pathSteering.CurrentPath != null) { // Attack doors - if (canAttackSub) + if (canAttackDoors) { // If the target is in the same hull, there shouldn't be any doors blocking the path if (targetCharacter == null || targetCharacter.CurrentHull != Character.CurrentHull) { var door = pathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? pathSteering.CurrentPath.NextNode?.ConnectedDoor; - if (door != null && !door.IsOpen) + if (door != null && !door.IsOpen && !door.IsBroken) { if (door.Item.AiTarget != null && SelectedAiTarget != door.Item.AiTarget) { @@ -1045,15 +1065,17 @@ namespace Barotrauma } } // Steer towards the target if in the same room and swimming - if ((Character.AnimController.InWater || pursue) && targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull)) + if ((Character.AnimController.InWater || pursue || !Character.AnimController.CanWalk) && + (targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull) || Character.CanSeeTarget(SelectedAiTarget.Entity))) { - SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(attackSimPos - steeringLimb.SimPosition)); + Vector2 myPos = Character.AnimController.SimplePhysicsEnabled ? Character.SimPosition : steeringLimb.SimPosition; + SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(steerPos - myPos)); } 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) + if ((!canAttackWalls || wallTarget == null) && !pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable) { State = AIState.Idle; return; @@ -1112,6 +1134,8 @@ namespace Barotrauma return false; } + private readonly List attackLimbs = new List(); + private readonly List weights = new List(); private Limb GetAttackLimb(Vector2 attackWorldPos, Limb ignoredLimb = null) { var currentContexts = Character.GetAttackContexts(); @@ -1127,23 +1151,38 @@ namespace Barotrauma if (attack == null) { continue; } if (attack.CoolDownTimer > 0) { continue; } if (!attack.IsValidContext(currentContexts)) { continue; } - if (!attack.IsValidTarget(target)) { continue; } + if (!attack.IsValidTarget(target as IDamageable)) { continue; } if (target is ISerializableEntity se && target is Character) { if (attack.Conditionals.Any(c => !c.Matches(se))) { continue; } } if (attack.Conditionals.Any(c => c.TargetSelf && !c.Matches(Character))) { continue; } - float priority = CalculatePriority(limb, attackWorldPos); - if (priority > currentPriority) + if (AIParams.RandomAttack) { - currentPriority = priority; - selectedLimb = limb; + attackLimbs.Add(limb); + weights.Add(limb.attack.Priority); } + else + { + float priority = CalculatePriority(limb, attackWorldPos); + if (priority > currentPriority) + { + currentPriority = priority; + selectedLimb = limb; + } + } + } + if (AIParams.RandomAttack) + { + selectedLimb = ToolBox.SelectWeightedRandom(attackLimbs, weights, Rand.RandSync.Server); + attackLimbs.Clear(); + weights.Clear(); } return selectedLimb; float CalculatePriority(Limb limb, Vector2 attackPos) { + if (Character.AnimController.SimplePhysicsEnabled) { return 1 + limb.attack.Priority; } float dist = Vector2.Distance(limb.WorldPosition, attackPos); // The limb is ignored if the target is not close. Prevents character going in reverse if very far away from it. // We also need a max value that is more than the actual range. @@ -1171,7 +1210,7 @@ namespace Barotrauma Body closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true, ignoreSensors: CanEnterSubmarine, ignoreDisabledWalls: CanEnterSubmarine); if (Submarine.LastPickedFraction != 1.0f && closestBody != null) { - if (closestBody.UserData is Structure wall && wall.Submarine != null) + if (closestBody.UserData is Structure wall && wall.Submarine != null && (wall.Submarine.Info.IsPlayer || wall.Submarine.Info.IsOutpost && TargetOutposts)) { int sectionIndex = wall.FindSectionIndex(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition)); float sectionDamage = wall.SectionDamage(sectionIndex); @@ -1210,7 +1249,10 @@ namespace Barotrauma LatchOntoAI?.SetAttachTarget(wall.Submarine.PhysicsBody.FarseerBody, wall.Submarine, ConvertUnits.ToSimUnits(sectionPos), attachTargetNormal); if (Character.AnimController.CanEnterSubmarine || !wall.SectionBodyDisabled(sectionIndex) && !IsWallDisabled(wall)) { - wallTarget = new WallTarget(sectionPos, wall, sectionIndex); + if (AIParams.TargetOuterWalls || wall.prefab.Tags.Contains("inner")) + { + wallTarget = new WallTarget(sectionPos, wall, sectionIndex); + } } } if (!Character.AnimController.CanEnterSubmarine && wallTarget == null) @@ -1239,7 +1281,7 @@ namespace Barotrauma } return isDisabled; } - + public override void OnAttacked(Character attacker, AttackResult attackResult) { float reactionTime = Rand.Range(0.1f, 0.3f); @@ -1249,22 +1291,28 @@ namespace Barotrauma Character.AnimController.ReleaseStuckLimbs(); LatchOntoAI?.DeattachFromBody(); if (attacker == null || attacker.AiTarget == null) { return; } + bool isFriendly = IsFriendly(Character, attacker); if (wasLatched) { avoidTimer = avoidTime * Rand.Range(0.75f, 1.25f); - SelectTarget(attacker.AiTarget); + if (!isFriendly) + { + SelectTarget(attacker.AiTarget); + } return; } if (State == AIState.Flee) { - SelectTarget(attacker.AiTarget); + if (!isFriendly) + { + SelectTarget(attacker.AiTarget); + } return; } - - if (attackResult.Damage > 0.0f) + if (!isFriendly && attackResult.Damage > 0.0f) { - bool canAttack = attacker.Submarine == Character.Submarine && canAttackCharacters || attacker.Submarine != null && canAttackSub; + bool canAttack = attacker.Submarine == Character.Submarine && canAttackCharacters || attacker.Submarine != null && canAttackWalls; if (Character.Params.AI.AttackWhenProvoked && canAttack) { if (attacker.IsHusk) @@ -1322,7 +1370,7 @@ namespace Barotrauma targetMemory.Priority += GetRelativeDamage(attackResult.Damage, Character.Vitality) * AggressionHurt; // Only allow to react once. Otherwise would attack the target with only a fraction of a cooldown - bool retaliate = SelectedAiTarget != attacker.AiTarget && attacker.Submarine == Character.Submarine; + bool retaliate = !isFriendly && SelectedAiTarget != attacker.AiTarget && attacker.Submarine == Character.Submarine; bool avoidGunFire = Character.Params.AI.AvoidGunfire && attacker.Submarine != Character.Submarine; if (State == AIState.Attack && !IsCoolDownRunning) @@ -1367,6 +1415,8 @@ namespace Barotrauma IDamageable damageTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity as IDamageable; if (damageTarget != null) { + //simulate attack input to get the character to attack client-side + Character.SetInput(InputType.Attack, true, true); if (attackingLimb.UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance, targetLimb)) { if (damageTarget.Health > 0) @@ -1424,7 +1474,7 @@ namespace Barotrauma State = AIState.Idle; return; } - Vector2 mouthPos = Character.AnimController.GetMouthPosition().Value; + Vector2 mouthPos = Character.AnimController.SimplePhysicsEnabled ? SimPosition : Character.AnimController.GetMouthPosition().Value; Vector2 attackSimPosition = Character.GetRelativeSimPosition(target); Vector2 limbDiff = attackSimPosition - mouthPos; float extent = Math.Max(mouthLimb.body.GetMaxExtent(), 2); @@ -1453,6 +1503,25 @@ namespace Barotrauma #endregion + private void UpdateFollow(float deltaTime) + { + if (SelectedAiTarget == null || SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed) + { + State = AIState.Idle; + return; + } + Vector2 dir = Vector2.Normalize(SelectedAiTarget.Entity.WorldPosition - Character.WorldPosition); + if (!MathUtils.IsValid(dir)) + { + return; + } + steeringManager.SteeringManual(deltaTime, dir); + if (Character.AnimController.InWater) + { + SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 15); + } + } + #region Targeting private bool IsLatchedOnSub => LatchOntoAI != null && LatchOntoAI.IsAttachedToSub; @@ -1579,13 +1648,16 @@ namespace Barotrauma Door door = null; if (aiTarget.Entity is Item item) { - //item inside and we're outside -> attack the hull - if (item.CurrentHull != null && character.CurrentHull == null) - { - targetingTag = "room"; - } - door = item.GetComponent(); + bool targetingFromOutsideToInside = item.CurrentHull != null && character.CurrentHull == null; + if (targetingFromOutsideToInside) + { + if (door != null && !canAttackDoors || !canAttackWalls) + { + // Can't reach + continue; + } + } foreach (var prio in AIParams.Targets) { if (item.HasTag(prio.Tag)) @@ -1594,7 +1666,25 @@ namespace Barotrauma break; } } - + if (door == null && targetingTag == null) + { + if (item.GetComponent() != null) + { + targetingTag = "sonar"; + } + else if (targetingFromOutsideToInside) + { + targetingTag = "room"; + } + } + else if (targetingTag == "nasonov") + { + if ((item.Submarine == null || !item.Submarine.Info.IsPlayer) && item.ParentInventory == null) + { + // Only target nasonovartifacts when they are held be a player or inside the playersub + continue; + } + } // Ignore the target if it's a decoy and the character is already inside a sub if (character.CurrentHull != null && targetingTag == "decoy") { @@ -1609,15 +1699,13 @@ namespace Barotrauma // Ignore structures that doesn't have a body (not walls) continue; } - if (s.IsPlatform) + if (s.IsPlatform) { continue; } + if (s.Submarine == null) { continue; } + bool isCharacterInside = character.CurrentHull != null; + bool isInnerWall = s.prefab.Tags.Contains("inner"); + if (isInnerWall && !isCharacterInside) { - continue; - } - bool isCharacterOutside = s.Submarine == null || character.CurrentHull == null; - bool targetInnerWalls = AIParams.TargetInnerWalls; - if (!isCharacterOutside && !targetInnerWalls) - { - // Ignore walls when inside (walltargets still work) + // Ignore inner walls when outside (walltargets still work) continue; } valueModifier = 1; @@ -1630,39 +1718,70 @@ namespace Barotrauma var section = s.Sections[i]; if (section.gap == null) { continue; } bool leadsInside = !section.gap.IsRoomToRoom && section.gap.FlowTargetHull != null; + isInnerWall = isInnerWall || !leadsInside; if (Character.AnimController.CanEnterSubmarine) { - if (isCharacterOutside) + if (!isCharacterInside) { if (CanPassThroughHole(s, i)) { - valueModifier *= leadsInside ? (AggressiveBoarding ? 5 : 1) : (targetInnerWalls ? 1 : 0); + valueModifier *= leadsInside ? (AggressiveBoarding ? 5 : 1) : 0; + } + else if (AggressiveBoarding && leadsInside && canAttackWalls && AIParams.TargetOuterWalls) + { + // Up to 100% priority increase for every gap in the wall when an aggressive boarder is outside + valueModifier *= 1 + section.gap.Open; + } + } + else + { + // Inside + if (AggressiveBoarding) + { + if (!isInnerWall) + { + // Only interested in getting inside (aggressive boarder) -> don't target outer walls when already inside + valueModifier = 0; + break; + } + else if (CanPassThroughHole(s, i)) + { + valueModifier *= isInnerWall ? 1 : 0; + } + else if (!canAttackWalls) + { + valueModifier = 0; + break; + } } 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) + if (!canAttackWalls) { - continue; - } - if (AggressiveBoarding && leadsInside) - { - // Up to 100% priority increase for every gap in the wall when an aggressive boarder is outside - valueModifier *= 1 + section.gap.Open; + valueModifier = 0; + break; } + // We are actually interested in breaking things -> reduce the priority when the wall is already broken + // (Terminalcells) + valueModifier *= 1 - section.gap.Open * 0.25f; } } - else if (!canAttackSub || CanPassThroughHole(s, i)) - { - // Already inside -> ignore holes in the walls and ignore walls if cannot attack the sub. - continue; - } } - else if (!leadsInside || !canAttackSub) + else { - // Can't get in, ignore inner walls - // Also ignore all walls if cannot attack the sub - continue; + // Cannot enter + if (isInnerWall || !canAttackWalls) + { + // Ignore inner walls and all walls if cannot do damage on walls. + valueModifier = 0; + break; + } + else if (AggressiveBoarding) + { + // Up to 100% priority increase for every gap in the wall when an aggressive boarder is outside + // (Bonethreshers) + valueModifier *= 1 + section.gap.Open; + } } } } @@ -1679,31 +1798,34 @@ namespace Barotrauma } if (door.Item.Submarine == null) { continue;} bool isOutdoor = door.LinkedGap?.FlowTargetHull != null && !door.LinkedGap.IsRoomToRoom; - bool isOpen = door.IsOpen; - if (!isOpen && (!canAttackSub)) + bool isOpen = door.IsOpen || door.IsBroken; + if (!isOpen && !canAttackDoors || (isOutdoor && !AIParams.TargetOuterWalls)) { - // 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; + // Ignore doors that are not open if cannot attack doors or shouldn't target outer doors. + continue; } - if (character.CurrentHull == null) + if (isOpen && (!Character.AnimController.CanEnterSubmarine || !AggressiveBoarding)) { - valueModifier = isOutdoor ? 1 : 0; + // Ignore broken and open doors + // Aggressive boarders don't ignore open doors, because they use them for get in. + continue; } - else if (AggressiveBoarding) + if (AggressiveBoarding) { - // Increase priority if the character is outside and an aggressive boarder, and the door is from outside to inside - if (character.CurrentHull == null) + // Increase the priority if the character is outside and the door is from outside to inside + if (character.CurrentHull == null && isOutdoor) { valueModifier *= isOpen ? 5 : 1; } else { - valueModifier *= isOpen ? 0 : 1; + // Inside + valueModifier *= isOpen || isOutdoor ? 0 : 1; } } - else if (!Character.AnimController.CanEnterSubmarine && isOpen) //ignore broken and open doors + else if (character.CurrentHull == null) { - continue; + valueModifier = isOutdoor ? 1 : 0; } } else if (aiTarget.Entity is IDamageable targetDamageable && targetDamageable.Health <= 0.0f) @@ -1745,9 +1867,18 @@ namespace Barotrauma if (valueModifier > targetValue) { - // Don't target items that we own. - // This is a rare case, and almost entirely related to Humanhusks, so let's check it last to reduce unnecessary checks (although the check shouldn't be expensive) - if (aiTarget.Entity is Item i && i.IsOwnedBy(character)) { continue; } + if (aiTarget.Entity is Item i) + { + Character owner = GetOwner(i); + // Don't target items that we own. + // This is a rare case, and almost entirely related to Humanhusks, so let's check it last to reduce unnecessary checks (although the check shouldn't be expensive) + if (owner == character) { continue; } + if (owner != null && IsFriendly(Character, owner)) + { + // If the item is held by a friendly character, ignore it. + continue; + } + } if (targetCharacter != null) { if (targetCharacter.Submarine != Character.Submarine) @@ -1772,7 +1903,7 @@ namespace Barotrauma foreach (var gap in Character.CurrentHull.ConnectedGaps) { var door = gap.ConnectedDoor; - if (door == null || !door.IsOpen) + if (door == null || !door.IsOpen && !door.IsBroken) { var wall = gap.ConnectedWall; if (wall != null) @@ -1966,11 +2097,17 @@ namespace Barotrauma { // If the target is shooting from the submarine, we might not perceive it because it doesn't move. // --> Target the submarine too. - if (target.Submarine != null && canAttackSub) + if (target.Submarine != null && (canAttackDoors || canAttackWalls)) { ChangeParams("room", state, priority); - ChangeParams("wall", state, priority); - ChangeParams("door", state, priority); + if (canAttackWalls) + { + ChangeParams("wall", state, priority); + } + if (canAttackDoors) + { + ChangeParams("door", state, priority); + } } ChangeParams("provocative", state, priority, onlyExisting: true); ChangeParams("light", state, priority, onlyExisting: true); @@ -1991,7 +2128,7 @@ namespace Barotrauma Character.AnimController.ReleaseStuckLimbs(); escapeTarget = null; AttackingLimb = null; - escapeMargin = 0; + movementMargin = 0; allGapsSearched = false; unreachableGaps.Clear(); if (isStateChanged && to == AIState.Idle && from != to) @@ -2016,28 +2153,66 @@ namespace Barotrauma } } + public void ReevaluateAttacks() + { + canAttackWalls = LatchOntoAI != null && LatchOntoAI.AttachToSub; + canAttackDoors = false; + canAttackCharacters = false; + foreach (var limb in Character.AnimController.Limbs) + { + if (limb.IsSevered) { continue; } + if (limb.attack == null) { continue; } + if (!canAttackWalls) + { + canAttackWalls = limb.attack.IsValidTarget(AttackTarget.Structure) && limb.attack.StructureDamage > 0; + } + if (!canAttackDoors) + { + canAttackDoors = limb.attack.IsValidTarget(AttackTarget.Structure) && limb.attack.ItemDamage > 0; + } + if (!canAttackCharacters) + { + canAttackCharacters = limb.attack.IsValidTarget(AttackTarget.Character); + } + } + if (PathSteering != null) + { + PathSteering.CanBreakDoors = canAttackDoors; + } + } + + private Vector2 returnDir; + private float returnTimer; private void SteerInsideLevel(float deltaTime) { - if (Level.Loaded == null) { return; } - - Vector2 levelSimSize = new Vector2( - ConvertUnits.ToSimUnits(Level.Loaded.Size.X), - ConvertUnits.ToSimUnits(Level.Loaded.Size.Y)); - - float margin = 10.0f; - - if (SimPosition.Y < 0.0) + if (SteeringManager is IndoorsSteeringManager) { return; } + if (Level.Loaded == null) { return; } + Vector2 levelSimSize = ConvertUnits.ToSimUnits(Level.Loaded.Size.X, Level.Loaded.Size.Y); + float returnTime = 3; + if (SimPosition.Y < 0) { - steeringManager.SteeringManual(deltaTime, Vector2.UnitY * MathUtils.InverseLerp(0.0f, -margin, SimPosition.Y)); + // Too far down + returnTimer = returnTime * Rand.Range(0.75f, 1.25f); + returnDir = Vector2.UnitY; } - if (SimPosition.X < 0.0f) + if (SimPosition.X < 0) { - steeringManager.SteeringManual(deltaTime, Vector2.UnitX * MathUtils.InverseLerp(0.0f, -margin, SimPosition.X)); + // Too far left + returnTimer = returnTime * Rand.Range(0.75f, 1.25f); + returnDir = Vector2.UnitX; } if (SimPosition.X > levelSimSize.X) { - steeringManager.SteeringManual(deltaTime, Vector2.UnitX * MathUtils.InverseLerp(levelSimSize.X, levelSimSize.X + margin, SimPosition.X)); - } + // Too far right + returnTimer = returnTime * Rand.Range(0.75f, 1.25f); + returnDir = -Vector2.UnitX; + } + if (returnTimer > 0) + { + returnTimer -= deltaTime; + SteeringManager.Reset(); + SteeringManager.SteeringManual(deltaTime, returnDir); + } } private bool CanPassThroughHole(Structure wall, int sectionIndex) @@ -2068,7 +2243,6 @@ namespace Barotrauma targetLimbs.Clear(); foreach (var limb in target.AnimController.Limbs) { - if (limb.IsSevered) { continue; } if (limb.type == targetLimbType || targetLimbType == LimbType.None) { targetLimbs.Add(limb); @@ -2083,6 +2257,7 @@ namespace Barotrauma Limb targetLimb = null; foreach (Limb limb in targetLimbs) { + if (limb.IsSevered) { continue; } float dist = Vector2.DistanceSquared(limb.WorldPosition, attackLimb.WorldPosition) / Math.Max(limb.AttackPriority, 0.1f); if (dist < closestDist) { @@ -2092,6 +2267,27 @@ namespace Barotrauma } return targetLimb; } + + private Character GetOwner(Item item) + { + // If the item is held by a character, attack the character instead. + var pickable = item.GetComponent(); + if (pickable != null) + { + Character owner = pickable.Picker ?? item.FindParentInventory(i => i.Owner is Character)?.Owner as Character; + if (owner != null) + { + var target = owner.AiTarget; + if (target?.Entity != null && !target.Entity.Removed) + { + return owner; + } + } + } + return null; + } + + public static bool IsFriendly(Character me, Character other) => other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group); } //the "memory" of the Character diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 6e6a5fbc9..08a666469 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -30,8 +30,9 @@ namespace Barotrauma public static float HULL_SAFETY_THRESHOLD = 50; - public HashSet UnreachableHulls { get; private set; } = new HashSet(); - public HashSet UnsafeHulls { get; private set; } = new HashSet(); + public readonly HashSet UnreachableHulls = new HashSet(); + public readonly HashSet UnsafeHulls = new HashSet(); + public readonly List IgnoredItems = new List(); private SteeringManager outsideSteering, insideSteering; @@ -55,13 +56,13 @@ namespace Barotrauma private set; } - public float CurrentHullSafety { get; private set; } + public float CurrentHullSafety { get; private set; } = 100; public HumanAIController(Character c) : base(c) { if (!c.IsHuman) { - throw new System.Exception($"Tried to create a human ai controller for a non-human: {c.SpeciesName}!"); + throw new Exception($"Tried to create a human ai controller for a non-human: {c.SpeciesName}!"); } insideSteering = new IndoorsSteeringManager(this, true, false); outsideSteering = new SteeringManager(this); @@ -85,7 +86,7 @@ namespace Barotrauma { unreachableClearTimer = clearUnreachableInterval; UnreachableHulls.Clear(); - ignoredContainers.Clear(); + IgnoredItems.Clear(); } // Use the pathfinding also outside of the sub, but not farther than the extents of the sub + 500 units. @@ -175,9 +176,7 @@ namespace Barotrauma } steeringManager.Update(Character.AnimController.GetCurrentSpeed(run && Character.CanRun)); - bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && - (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X)); - + bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X)); if (steeringManager == insideSteering) { var currPath = PathSteering.CurrentPath; @@ -185,51 +184,24 @@ namespace Barotrauma { if (currPath.CurrentNode.SimPosition.Y < Character.AnimController.GetColliderBottom().Y) { - // Don't allow to jump from too high. The formula might require tweaking. + // Don't allow to jump from too high. float allowedJumpHeight = Character.AnimController.ImpactTolerance / 2; float height = Math.Abs(currPath.CurrentNode.SimPosition.Y - Character.SimPosition.Y); ignorePlatforms = height < allowedJumpHeight; } } - if (Character.IsClimbing && PathSteering.IsNextLadderSameAsCurrent) { Character.AnimController.TargetMovement = new Vector2(0.0f, Math.Sign(Character.AnimController.TargetMovement.Y)); } } - Character.AnimController.IgnorePlatforms = ignorePlatforms; Vector2 targetMovement = AnimController.TargetMovement; - if (!Character.AnimController.InWater) { targetMovement = new Vector2(Character.AnimController.TargetMovement.X, MathHelper.Clamp(Character.AnimController.TargetMovement.Y, -1.0f, 1.0f)); } - - 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 = Character.ApplyMovementLimits(targetMovement, AnimController.GetCurrentSpeed(run)); flipTimer -= deltaTime; @@ -280,14 +252,14 @@ namespace Barotrauma else { findItemState = FindItemState.Extinguisher; - if (FindSuitableContainer(Character, extinguisher, out Item targetContainer)) + if (FindSuitableContainer(extinguisher, out Item targetContainer)) { findItemState = FindItemState.None; itemIndex = 0; if (targetContainer != null) { var decontainObjective = new AIObjectiveDecontainItem(Character, extinguisher, ObjectiveManager, targetContainer: targetContainer.GetComponent()); - decontainObjective.Abandoned += () => ignoredContainers.Add(targetContainer); + decontainObjective.Abandoned += () => IgnoredItems.Add(targetContainer); ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true); return; } @@ -310,42 +282,47 @@ namespace Barotrauma || ObjectiveManager.IsCurrentObjective() || ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn); bool removeDivingSuit = !Character.AnimController.HeadInWater && oxygenLow; - AIObjectiveGoTo gotoObjective = ObjectiveManager.GetActiveObjective(); + bool takeMaskOff = !Character.AnimController.HeadInWater && oxygenLow; if (!removeDivingSuit) { - 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) + if (shouldKeepTheGearOn) { - if (NeedsDivingGear(Character, h, out _)) - { - removeDivingSuit = false; - takeMaskOff = false; - } + removeDivingSuit = false; } - else if (gotoObjective.Target is Character c) + } + if (!takeMaskOff) + { + if (shouldKeepTheGearOn) { - if (NeedsDivingGear(Character, c.CurrentHull, out _)) - { - removeDivingSuit = false; - takeMaskOff = false; - } + takeMaskOff = false; } - else if (gotoObjective.Target is Item i) + } + if (!shouldKeepTheGearOn && (!takeMaskOff || !removeDivingSuit)) + { + foreach (var objective in ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(includingSelf: true)) { - if (NeedsDivingGear(Character, i.CurrentHull, out _)) + if (objective is AIObjectiveGoTo gotoObjective) { - removeDivingSuit = false; - takeMaskOff = false; + bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty; + Hull targetHull = gotoObjective.GetTargetHull(); + bool targetIsOutside = (gotoObjective.Target != null && targetHull == null) || (insideSteering && PathSteering.CurrentPath.HasOutdoorsNodes); + if (targetIsOutside || NeedsDivingGear(Character, targetHull, out _)) + { + removeDivingSuit = false; + takeMaskOff = false; + break; + } + else if (gotoObjective.mimic) + { + if (!removeDivingSuit) + { + removeDivingSuit = !HasDivingSuit(gotoObjective.Target as Character); + } + if (!takeMaskOff) + { + takeMaskOff = !HasDivingMask(gotoObjective.Target as Character); + } + } } } } @@ -363,7 +340,7 @@ namespace Barotrauma else { findItemState = FindItemState.DivingSuit; - if (FindSuitableContainer(Character, divingSuit, out Item targetContainer)) + if (FindSuitableContainer(divingSuit, out Item targetContainer)) { findItemState = FindItemState.None; itemIndex = 0; @@ -375,7 +352,7 @@ namespace Barotrauma }; decontainObjective.Abandoned += () => { - ignoredContainers.Add(targetContainer); + IgnoredItems.Add(targetContainer); }; ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true); return; @@ -405,14 +382,14 @@ namespace Barotrauma else { findItemState = FindItemState.DivingMask; - if (FindSuitableContainer(Character, mask, out Item targetContainer)) + if (FindSuitableContainer(mask, out Item targetContainer)) { findItemState = FindItemState.None; itemIndex = 0; if (targetContainer != null) { var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent()); - decontainObjective.Abandoned += () => ignoredContainers.Add(targetContainer); + decontainObjective.Abandoned += () => IgnoredItems.Add(targetContainer); ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true); return; } @@ -442,14 +419,14 @@ namespace Barotrauma { if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List() { InvSlotType.Any })) { - if (FindSuitableContainer(Character, item, out Item targetContainer)) + if (FindSuitableContainer(item, out Item targetContainer)) { findItemState = FindItemState.None; itemIndex = 0; if (targetContainer != null) { var decontainObjective = new AIObjectiveDecontainItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent()); - decontainObjective.Abandoned += () => ignoredContainers.Add(targetContainer); + decontainObjective.Abandoned += () => IgnoredItems.Add(targetContainer); ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true); return; } @@ -478,11 +455,10 @@ namespace Barotrauma } private FindItemState findItemState; private int itemIndex; - private List ignoredContainers = new List(); - public bool FindSuitableContainer(Character character, Item containableItem, out Item suitableContainer) + public bool FindSuitableContainer(Item containableItem, out Item suitableContainer) { suitableContainer = null; - if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: ignoredContainers, customPriorityFunction: i => + if (Character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: IgnoredItems, customPriorityFunction: i => { var container = i.GetComponent(); if (container == null) { return 0; } @@ -645,7 +621,7 @@ namespace Barotrauma // Should not cancel any existing ai objectives (so that if the character attacked you and then helped, we still would want to retaliate). return; } - if (!attacker.IsPlayer && attacker.AIController != null && attacker.AIController.Enabled) + if (attacker.IsBot) { // Don't retaliate on damage done by friendly ai, because we know that it's accidental AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, Rand.Range(0.5f, 1f, Rand.RandSync.Unsynced)); @@ -784,7 +760,6 @@ namespace Barotrauma return false; } - public static bool HasDivingGear(Character character, float conditionPercentage = 0) => HasDivingSuit(character, conditionPercentage) || HasDivingMask(character, conditionPercentage); /// @@ -1055,13 +1030,11 @@ namespace Barotrauma public static bool IsItemOperatedByAnother(Character character, ItemComponent target, out Character operatingCharacter) { operatingCharacter = null; + if (target?.Item == null) { return false; } foreach (var c in Character.CharacterList) { - if (character != null) - { - if (c == character) { continue; } - if (!IsFriendly(character, c)) { continue; } - } + if (character != null && c == character) { continue; } + if (character?.AIController is HumanAIController humanAi && !humanAi.IsFriendly(c)) { continue; } if (c.SelectedConstruction != target.Item) { continue; } operatingCharacter = c; // If the other character is player, don't try to operate diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index 6628d279f..ad515afa2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -12,7 +12,8 @@ namespace Barotrauma private PathFinder pathFinder; private SteeringPath currentPath; - private bool canOpenDoors, canBreakDoors; + private bool canOpenDoors; + public bool CanBreakDoors { get; set; } private Character character; @@ -50,8 +51,8 @@ namespace Barotrauma /// public bool InLadders => currentPath != null && - currentPath.CurrentNode != null && (currentPath.CurrentNode.Ladders != null || - (currentPath.NextNode != null && currentPath.NextNode.Ladders != null)); + currentPath.CurrentNode != null && (currentPath.CurrentNode.Ladders != null && !currentPath.CurrentNode.Ladders.Item.NonInteractable || + (currentPath.NextNode != null && currentPath.NextNode.Ladders != null && !currentPath.NextNode.Ladders.Item.NonInteractable)); /// /// Returns true if any node in the path is in stairs @@ -69,6 +70,7 @@ namespace Barotrauma if (currentPath.NextNode == null) { return false; } var currentLadder = currentPath.CurrentNode.Ladders; if (currentLadder == null) { return false; } + if (currentLadder.Item.NonInteractable) { return false; } var nextLadder = GetNextLadder(); return nextLadder != null && nextLadder == currentLadder; } @@ -80,7 +82,7 @@ namespace Barotrauma pathFinder.GetNodePenalty = GetNodePenalty; this.canOpenDoors = canOpenDoors; - this.canBreakDoors = canBreakDoors; + this.CanBreakDoors = canBreakDoors; character = (host as AIController).Character; @@ -103,6 +105,12 @@ namespace Barotrauma IsPathDirty = false; } + public void ResetPath() + { + currentPath = null; + IsPathDirty = true; + } + public void SteeringSeek(Vector2 target, float weight, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null) { steering += CalculateSteeringSeek(target, weight, startNodeFilter, endNodeFilter, nodeFilter); @@ -115,7 +123,7 @@ namespace Barotrauma { if (currentPath == null) { return null; } if (currentPath.NextNode == null) { return null; } - if (currentPath.NextNode.Ladders != null) + if (currentPath.NextNode.Ladders != null && !currentPath.NextNode.Ladders.Item.NonInteractable) { return currentPath.NextNode.Ladders; } @@ -126,7 +134,10 @@ namespace Barotrauma { var node = currentPath.Nodes[index]; if (node == null) { return null; } - return node.Ladders; + if (node.Ladders != null && !node.Ladders.Item.NonInteractable) + { + return node.Ladders; + } } return null; } @@ -134,7 +145,19 @@ namespace Barotrauma private Vector2 CalculateSteeringSeek(Vector2 target, float weight, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null) { - bool needsNewPath = character.Params.PathFinderPriority > 0.5f && (currentPath == null || currentPath.Unreachable || currentPath.Finished || Vector2.DistanceSquared(target, currentTarget) > 1); + Vector2 targetDiff = target - currentTarget; + if (currentPath != null && currentPath.Nodes.Any()) + { + //current path calculated relative to a different sub than where the character is now + //take that into account when calculating if the target has moved + Submarine currentPathSub = currentPath?.Nodes.First().Submarine; + if (currentPathSub != character.Submarine && character.Submarine != null) + { + Vector2 subDiff = character.Submarine.SimPosition - currentPathSub.SimPosition; + targetDiff += subDiff; + } + } + bool needsNewPath = character.Params.PathFinderPriority > 0.5f && (currentPath == null || currentPath.Unreachable || currentPath.Finished || targetDiff.LengthSquared() > 1); //find a new path if one hasn't been found yet or the target is different from the current target if (needsNewPath || findPathTimer < -1.0f) { @@ -172,12 +195,13 @@ namespace Barotrauma Vector2 diff = DiffToCurrentNode(); var collider = character.AnimController.Collider; + // Only humanoids can climb ladders + bool canClimb = character.AnimController is HumanoidAnimController; //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) + if (canClimb && !character.AnimController.InWater && !character.IsClimbing && diff.Y < collider.height / 2 + collider.radius) { diff.Y = 0.0f; } - //if (diff.LengthSquared() < 0.001f) { return -host.Steering; } if (diff == Vector2.Zero) { return Vector2.Zero; } return Vector2.Normalize(diff) * weight; } @@ -186,8 +210,10 @@ namespace Barotrauma private Vector2 DiffToCurrentNode() { - if (currentPath == null || currentPath.Unreachable) return Vector2.Zero; - + if (currentPath == null || currentPath.Unreachable) + { + return Vector2.Zero; + } if (currentPath.Finished) { Vector2 pos2 = host.SimPosition; @@ -197,15 +223,12 @@ namespace Barotrauma pos2 -= CurrentPath.Nodes.Last().Submarine.SimPosition; } return currentTarget - pos2; - } - + } if (canOpenDoors && !character.LockHands && buttonPressCooldown <= 0.0f) { CheckDoorsInPath(); - } - + } Vector2 pos = host.SimPosition; - if (character != null && currentPath.CurrentNode != null) { if (CurrentPath.CurrentNode.Submarine != null) @@ -220,19 +243,17 @@ namespace Barotrauma } } } - bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater; - - //only humanoids can climb ladders - if (!isDiving && character.AnimController is HumanoidAnimController && IsNextLadderSameAsCurrent) + // Only humanoids can climb ladders + bool canClimb = character.AnimController is HumanoidAnimController; + if (canClimb && !isDiving && IsNextLadderSameAsCurrent) { - if (character.SelectedConstruction != currentPath.CurrentNode.Ladders.Item && - currentPath.CurrentNode.Ladders.Item.IsInsideTrigger(character.WorldPosition)) + var ladders = currentPath.CurrentNode.Ladders; + if (character.SelectedConstruction != ladders.Item && ladders.Item.IsInsideTrigger(character.WorldPosition)) { currentPath.CurrentNode.Ladders.Item.TryInteract(character, false, true); } - } - + } var collider = character.AnimController.Collider; if (character.IsClimbing && !isDiving) { @@ -252,13 +273,16 @@ namespace Barotrauma { diff.Y = Math.Max(diff.Y, 1.0f); } + // 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 isAboveFloor = heightFromFloor > -margin && heightFromFloor < collider.height * 1.5f; // If the next waypoint is horizontally far, we don't want to keep holding the ladders - if (nextLadder == null || Math.Abs(currentPath.CurrentNode.WorldPosition.X - currentPath.NextNode.WorldPosition.X) > 50) + if (isAboveFloor && (nextLadder == null || Math.Abs(currentPath.CurrentNode.WorldPosition.X - currentPath.NextNode.WorldPosition.X) > 50)) { character.AnimController.Anim = AnimController.Animation.None; character.SelectedConstruction = null; } - else if (!nextLadderSameAsCurrent) + else if (nextLadder != null && !nextLadderSameAsCurrent) { // Try to change the ladder (hatches between two submarines) if (character.SelectedConstruction != nextLadder.Item && nextLadder.Item.IsInsideTrigger(character.WorldPosition)) @@ -266,9 +290,6 @@ namespace Barotrauma nextLadder.Item.TryInteract(character, false, true); } } - // 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 isAboveFloor = heightFromFloor > -margin && heightFromFloor < collider.height * 1.5f; if (nextLadder != null || isAboveFloor) { currentPath.SkipToNextNode(); @@ -286,7 +307,7 @@ namespace Barotrauma } return diff; } - else if (character.AnimController.InWater) + else if (!canClimb || character.AnimController.InWater) { // If the character is underwater, we don't need the ladders anymore if (character.IsClimbing && isDiving) @@ -294,49 +315,59 @@ namespace Barotrauma character.AnimController.Anim = AnimController.Animation.None; character.SelectedConstruction = null; } - float multiplier = MathHelper.Lerp(1, 10, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1)); - float targetDistance = collider.GetSize().X * multiplier; - float horizontalDistance = Math.Abs(character.WorldPosition.X - currentPath.CurrentNode.WorldPosition.X); - float verticalDistance = Math.Abs(character.WorldPosition.Y - currentPath.CurrentNode.WorldPosition.Y); - if (character.CurrentHull != currentPath.CurrentNode.CurrentHull) + var door = currentPath.CurrentNode.ConnectedDoor; + bool blockedByDoor = door != null && !door.IsOpen && !door.IsBroken; + if (!blockedByDoor) { - verticalDistance *= 2; - } - float distance = horizontalDistance + verticalDistance; - if (ConvertUnits.ToSimUnits(distance) < targetDistance) - { - currentPath.SkipToNextNode(); + float multiplier = MathHelper.Lerp(1, 10, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1)); + float targetDistance = collider.GetSize().X * multiplier; + float horizontalDistance = Math.Abs(character.WorldPosition.X - currentPath.CurrentNode.WorldPosition.X); + float verticalDistance = Math.Abs(character.WorldPosition.Y - currentPath.CurrentNode.WorldPosition.Y); + if (character.CurrentHull != currentPath.CurrentNode.CurrentHull) + { + verticalDistance *= 2; + } + float distance = horizontalDistance + verticalDistance; + if (ConvertUnits.ToSimUnits(distance) < targetDistance) + { + currentPath.SkipToNextNode(); + } } } else if (!IsNextLadderSameAsCurrent) { + // Walking horizontally Vector2 colliderBottom = character.AnimController.GetColliderBottom(); Vector2 colliderSize = collider.GetSize(); Vector2 velocity = collider.LinearVelocity; - // If the character is smaller than this, it fails to use the waypoint nodes, because they are always too high. + // If the character is smaller than this, it would fail to use the waypoint nodes because they are always too high. float minHeight = 1; // Cannot use the head position, because not all characters have head or it can be below the total height of the character float characterHeight = Math.Max(colliderSize.Y + character.AnimController.ColliderHeightFromFloor, minHeight); float horizontalDistance = Math.Abs(collider.SimPosition.X - currentPath.CurrentNode.SimPosition.X); bool isAboveFeet = currentPath.CurrentNode.SimPosition.Y > colliderBottom.Y; bool isNotTooHigh = currentPath.CurrentNode.SimPosition.Y < colliderBottom.Y + characterHeight; + var door = currentPath.CurrentNode.ConnectedDoor; + bool blockedByDoor = door != null && !door.IsOpen && !door.IsBroken; float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 10, 0, 1)); float targetDistance = collider.radius * margin; - if (horizontalDistance < targetDistance && isAboveFeet && isNotTooHigh) + if (horizontalDistance < targetDistance && isAboveFeet && isNotTooHigh && !blockedByDoor) { currentPath.SkipToNextNode(); } } - - if (currentPath.CurrentNode == null) return Vector2.Zero; - + if (currentPath.CurrentNode == null) + { + return Vector2.Zero; + } return currentPath.CurrentNode.SimPosition - pos; } private bool CanAccessDoor(Door door, Func buttonFilter = null) { if (door.IsOpen) { return true; } - if (canBreakDoors) { return true; } + if (door.Item.NonInteractable) { return false; } + if (CanBreakDoors) { return true; } if (door.IsStuck) { return false; } if (!canOpenDoors || character.LockHands) { return false; } if (door.HasIntegratedButtons) @@ -345,7 +376,7 @@ namespace Barotrauma } else { - return door.Item.GetConnectedComponents(true).Any(b => b.HasAccess(character) && (buttonFilter == null || buttonFilter(b))); + return door.Item.GetConnectedComponents(true).Any(b => !b.Item.NonInteractable && b.HasAccess(character) && (buttonFilter == null || buttonFilter(b))); } } @@ -519,9 +550,9 @@ namespace Barotrauma //non-humanoids can't climb up ladders if (!(character.AnimController is HumanoidAnimController)) { - if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null && - nextNode.Position.Y - node.Position.Y > 1.0f && //more than one sim unit to climb up - nextNode.Waypoint.CurrentHull != null && nextNode.Waypoint.CurrentHull.Surface < nextNode.Waypoint.Position.Y) //upper node not underwater + if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null && nextNode.Waypoint.Ladders.Item.NonInteractable || + (nextNode.Position.Y - node.Position.Y > 1.0f && //more than one sim unit to climb up + nextNode.Waypoint.CurrentHull != null && nextNode.Waypoint.CurrentHull.Surface < nextNode.Waypoint.Position.Y)) //upper node not underwater { return null; } @@ -539,7 +570,10 @@ namespace Barotrauma } if (character.NeedsAir && hull.WaterVolume / hull.Rect.Width > 100.0f) { - penalty += 500.0f; + if (!HumanAIController.HasDivingSuit(character)) + { + penalty += 500.0f; + } } if (character.PressureProtection < 10.0f && hull.WaterVolume > hull.Volume) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs index 68746f340..11fb2d14d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/LatchOntoAI.cs @@ -1,11 +1,11 @@ using FarseerPhysics; -using FarseerPhysics.Common; using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics.Joints; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Xml.Linq; +using System.Linq; namespace Barotrauma { @@ -19,8 +19,8 @@ namespace Barotrauma private Vector2 attachSurfaceNormal; private Submarine attachTargetSubmarine; - private bool attachToSub; - private bool attachToWalls; + public bool AttachToSub { get; private set; } + public bool AttachToWalls { get; private set; } private float minDeattachSpeed = 3.0f, maxDeattachSpeed = 10.0f; private float damageOnDetach = 0.0f, detachStun = 0.0f; @@ -58,8 +58,8 @@ namespace Barotrauma public LatchOntoAI(XElement element, EnemyAIController enemyAI) { - attachToWalls = element.GetAttributeBool("attachtowalls", false); - attachToSub = element.GetAttributeBool("attachtosub", false); + AttachToWalls = element.GetAttributeBool("attachtowalls", false); + AttachToSub = element.GetAttributeBool("attachtosub", false); minDeattachSpeed = element.GetAttributeFloat("mindeattachspeed", 3.0f); maxDeattachSpeed = Math.Max(minDeattachSpeed, element.GetAttributeFloat("maxdeattachspeed", 10.0f)); damageOnDetach = element.GetAttributeFloat("damageondetach", 0.0f); @@ -67,11 +67,19 @@ namespace Barotrauma localAttachPos = ConvertUnits.ToSimUnits(element.GetAttributeVector2("localattachpos", Vector2.Zero)); attachLimbRotation = MathHelper.ToRadians(element.GetAttributeFloat("attachlimbrotation", 0.0f)); - if (Enum.TryParse(element.GetAttributeString("attachlimb", "Head"), out LimbType attachLimbType)) + string limbString = element.GetAttributeString("attachlimb", null); + attachLimb = enemyAI.Character.AnimController.Limbs.FirstOrDefault(l => string.Equals(l.Name, limbString, StringComparison.OrdinalIgnoreCase)); + if (attachLimb == null) { - attachLimb = enemyAI.Character.AnimController.GetLimb(attachLimbType); + if (Enum.TryParse(limbString, out LimbType attachLimbType)) + { + attachLimb = enemyAI.Character.AnimController.GetLimb(attachLimbType); + } + } + if (attachLimb == null) + { + attachLimb = enemyAI.Character.AnimController.MainLimb; } - if (attachLimb == null) attachLimb = enemyAI.Character.AnimController.MainLimb; enemyAI.Character.OnDeath += OnCharacterDeath; } @@ -108,7 +116,9 @@ namespace Barotrauma //something went wrong, limb body is very far from the joint anchor -> deattach if (Vector2.DistanceSquared(attachJoints[i].WorldAnchorB, attachJoints[i].BodyA.Position) > 10.0f * 10.0f) { +#if DEBUG DebugConsole.ThrowError("Limb body of the character \"" + character.Name + "\" is very far from the attach joint anchor -> deattach"); +#endif DeattachFromBody(); return; } @@ -131,7 +141,7 @@ namespace Barotrauma switch (enemyAI.State) { case AIState.Idle: - if (attachToWalls && character.Submarine == null && Level.Loaded != null) + if (AttachToWalls && character.Submarine == null && Level.Loaded != null) { if (!IsAttached) { @@ -180,8 +190,9 @@ namespace Barotrauma } else { - float dist = Vector2.Distance(character.SimPosition, wallAttachPos); - if (dist < Math.Max(Math.Max(character.AnimController.Collider.radius, character.AnimController.Collider.width), character.AnimController.Collider.height) * 1.2f) + float squaredDistance = Vector2.DistanceSquared(character.SimPosition, wallAttachPos); + float targetDistance = Math.Max(Math.Max(character.AnimController.Collider.radius, character.AnimController.Collider.width), character.AnimController.Collider.height) * 1.2f; + if (squaredDistance < targetDistance * targetDistance) { //close enough to a wall -> attach AttachToBody(character.AnimController.Collider, attachLimb, attachTargetBody, wallAttachPos); @@ -197,12 +208,13 @@ namespace Barotrauma } break; case AIState.Attack: + case AIState.Aggressive: if (enemyAI.AttackingLimb != null) { - if (attachToSub && !enemyAI.IsSteeringThroughGap && wallAttachPos != Vector2.Zero && attachTargetBody != null) + if (AttachToSub && !enemyAI.IsSteeringThroughGap && wallAttachPos != Vector2.Zero && attachTargetBody != null) { // is not attached or is attached to something else - if (!IsAttached || IsAttached && attachJoints[0].BodyB == attachTargetBody) + if (!IsAttached || IsAttached && attachJoints[0].BodyB != attachTargetBody) { if (Vector2.DistanceSquared(ConvertUnits.ToDisplayUnits(transformedAttachPos), enemyAI.AttackingLimb.WorldPosition) < enemyAI.AttackingLimb.attack.DamageRange * enemyAI.AttackingLimb.attack.DamageRange) { @@ -247,16 +259,17 @@ namespace Barotrauma if (attachJoints.Count > 0) { //already attached to the target body, no need to do anything - if (attachJoints[0].BodyB == targetBody) return; + if (attachJoints[0].BodyB == targetBody) { return; } DeattachFromBody(); } jointDir = attachLimb.Dir; Vector2 transformedLocalAttachPos = localAttachPos * attachLimb.Scale * attachLimb.Params.Ragdoll.LimbScale; - if (jointDir < 0.0f) transformedLocalAttachPos.X = -transformedLocalAttachPos.X; - - //transformedLocalAttachPos = Vector2.Transform(transformedLocalAttachPos, Matrix.CreateRotationZ(attachLimb.Rotation)); + if (jointDir < 0.0f) + { + transformedLocalAttachPos.X = -transformedLocalAttachPos.X; + } float angle = MathUtils.VectorToAngle(-attachSurfaceNormal) - MathHelper.PiOver2 + attachLimbRotation * attachLimb.Dir; attachLimb.body.SetTransform(attachPos + attachSurfaceNormal * transformedLocalAttachPos.Length(), angle); @@ -274,7 +287,10 @@ namespace Barotrauma // Limb scale is already taken into account when creating the collider. Vector2 colliderFront = collider.GetLocalFront(); - if (jointDir < 0.0f) colliderFront.X = -colliderFront.X; + if (jointDir < 0.0f) + { + colliderFront.X = -colliderFront.X; + } collider.SetTransform(attachPos + attachSurfaceNormal * colliderFront.Length(), MathUtils.VectorToAngle(-attachSurfaceNormal) - MathHelper.PiOver2); var colliderJoint = new WeldJoint(collider.FarseerBody, targetBody, colliderFront, targetBody.GetLocalPoint(attachPos), false) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs index c5ab0ea33..b1fcd4848 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; @@ -375,9 +375,7 @@ namespace Barotrauma } } - StreamWriter file = new StreamWriter(@"NPCConversations.csv"); - file.WriteLine(sb.ToString()); - file.Close(); + File.WriteAllText("NPCConversations.csv", sb.ToString()); } private static void WriteConversation(System.Text.StringBuilder sb, NPCConversation conv, int depthIndex) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index 581ef4f49..42112ccfd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -30,6 +30,7 @@ namespace Barotrauma public virtual bool KeepDivingGearOn => false; public virtual bool UnequipItems => false; + public virtual bool AllowOutsideSubmarine => false; protected readonly List subObjectives = new List(); private float _cumulatedDevotion; @@ -117,7 +118,6 @@ namespace Barotrauma 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++) @@ -182,12 +182,26 @@ namespace Barotrauma } } + protected bool IsAllowed => AllowOutsideSubmarine || character.Submarine != null && character.Submarine.TeamID == character.TeamID && character.Submarine.Info.IsPlayer; + /// /// Call this only when the priority needs to be recalculated. Use the cached Priority property when you don't need to recalculate. /// public virtual float GetPriority() { - Priority = CumulatedDevotion * PriorityModifier; + if (!IsAllowed) + { + Priority = 0; + return Priority; + } + if (objectiveManager.CurrentOrder == this) + { + Priority = AIObjectiveManager.OrderPriority; + } + else + { + Priority = CumulatedDevotion; + } return Priority; } @@ -196,7 +210,7 @@ namespace Barotrauma var currentObjective = objectiveManager.CurrentObjective; if (currentObjective != null && (currentObjective == this || currentObjective.subObjectives.Any(so => so == this))) { - CumulatedDevotion += Devotion * PriorityModifier * deltaTime; + CumulatedDevotion += Devotion * deltaTime; } } @@ -204,11 +218,7 @@ namespace Barotrauma public virtual void Update(float deltaTime) { - if (objectiveManager.CurrentOrder == this) - { - Priority = AIObjectiveManager.OrderPriority; - } - else if (objectiveManager.WaitTimer <= 0) + if (objectiveManager.CurrentOrder != this && objectiveManager.WaitTimer <= 0) { UpdateDevotion(deltaTime); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs index 5defc9589..74e1ffd5e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs @@ -20,11 +20,16 @@ namespace Barotrauma { if (battery == null) { return false; } var item = battery.Item; + if (item.NonInteractable) { return false; } if (item.Submarine == null) { return false; } if (item.CurrentHull == null) { return false; } if (item.Submarine.TeamID != character.TeamID) { return false; } + if (character.Submarine != null) + { + if (item.Submarine.Info.Type != character.Submarine.Info.Type) { return false; } + if (!character.Submarine.IsEntityFoundOnThisSub(item, true)) { 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) && HumanAIController.IsActive(c))) { return false; } if (IsReady(battery)) { return false; } return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index 123530a7e..c68b18bdc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -13,6 +13,7 @@ namespace Barotrauma public override bool KeepDivingGearOn => true; public override bool IgnoreUnsafeHulls => true; + public override bool AllowOutsideSubmarine => true; private readonly CombatMode initialMode; @@ -120,6 +121,12 @@ namespace Barotrauma protected override bool Check() { + if (initialMode == CombatMode.Offensive && Mode != CombatMode.Offensive) + { + Abandon = true; + SteeringManager.Reset(); + return false; + } bool completed = (Enemy != null && (Enemy.Removed || Enemy.IsDead)) || (initialMode != CombatMode.Offensive && coolDownTimer <= 0); if (completed) { @@ -464,7 +471,6 @@ namespace Barotrauma SteeringManager.Reset(); return; } - retreatTarget = null; RemoveSubObjective(ref retreatObjective); RemoveSubObjective(ref seekAmmunition); @@ -481,9 +487,8 @@ namespace Barotrauma }, onAbandon: () => { - Mode = CombatMode.Defensive; + Abandon = true; SteeringManager.Reset(); - RemoveSubObjective(ref followTargetObjective); }); if (followTargetObjective != null) { @@ -592,10 +597,7 @@ namespace Barotrauma private void Attack(float deltaTime) { - float squaredDistance = Vector2.DistanceSquared(character.Position, Enemy.Position); character.CursorPosition = Enemy.Position; - float engageDistance = 500; - if (character.CurrentHull != Enemy.CurrentHull && squaredDistance > engageDistance * engageDistance) { return; } if (!character.CanSeeCharacter(Enemy)) { return; } if (Weapon.RequireAimToUse) { @@ -603,7 +605,7 @@ namespace Barotrauma if (SteeringManager == PathSteering) { var door = PathSteering.CurrentPath?.CurrentNode?.ConnectedDoor; - if (door != null && !door.IsOpen) + if (door != null && !door.IsOpen && !door.IsBroken) { isOperatingButtons = door.HasIntegratedButtons || door.Item.GetConnectedComponents(true).Any(); } @@ -625,7 +627,7 @@ namespace Barotrauma } if (WeaponComponent is MeleeWeapon meleeWeapon) { - if (squaredDistance <= meleeWeapon.Range * meleeWeapon.Range) + if (Vector2.DistanceSquared(character.Position, Enemy.Position) <= meleeWeapon.Range * meleeWeapon.Range) { character.SetInput(InputType.Shoot, false, true); Weapon.Use(deltaTime, character); @@ -635,7 +637,7 @@ namespace Barotrauma { if (WeaponComponent is RepairTool repairTool) { - if (squaredDistance > repairTool.Range * repairTool.Range) { return; } + if (Vector2.DistanceSquared(character.Position, Enemy.Position) > repairTool.Range * repairTool.Range) { return; } } if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.Position - Weapon.Position) < MathHelper.PiOver4) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index a77455b67..42f48b0da 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -158,7 +158,7 @@ namespace Barotrauma Abandon = true; }, onCompleted: () => { - if (getItemObjective.TargetItem != null) + if (getItemObjective?.TargetItem != null) { containedItems.Add(getItemObjective.TargetItem); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs index 8ccdf33c8..b6fb5ebce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -27,6 +27,11 @@ namespace Barotrauma public override float GetPriority() { + if (!IsAllowed) + { + Priority = 0; + return Priority; + } if (!objectiveManager.IsCurrentOrder() && Character.CharacterList.Any(c => c.CurrentHull == targetHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { @@ -101,7 +106,7 @@ namespace Barotrauma if (SteeringManager == PathSteering) { var door = PathSteering.CurrentPath?.CurrentNode?.ConnectedDoor; - if (door != null && !door.IsOpen) + if (door != null && !door.IsOpen && !door.IsBroken) { isOperatingButtons = door.HasIntegratedButtons || door.Item.GetConnectedComponents(true).Any(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs index fd6bc49ce..e41f6675f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs @@ -32,7 +32,11 @@ namespace Barotrauma if (hull.FireSources.None()) { return false; } if (hull.Submarine == null) { return false; } if (hull.Submarine.TeamID != character.TeamID) { return false; } - if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(hull, true)) { return false; } + if (character.Submarine != null) + { + if (hull.Submarine.Info.Type != character.Submarine.Info.Type) { return false; } + if (!character.Submarine.IsEntityFoundOnThisSub(hull, true)) { return false; } + } return true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs index a21460a6d..9d9f74c13 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs @@ -1,9 +1,4 @@ -using Barotrauma.Items.Components; -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Xna.Framework; -using Barotrauma.Extensions; +using System.Collections.Generic; namespace Barotrauma { @@ -40,7 +35,11 @@ namespace Barotrauma if (target.Submarine == null) { return false; } if (target.Submarine.TeamID != character.TeamID) { return false; } if (target.CurrentHull == null) { return false; } - if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, true)) { return false; } + if (character.Submarine != null) + { + if (target.Submarine.Info.Type != character.Submarine.Info.Type) { return false; } + if (!character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, true)) { return false; } + } return true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index 63eb1372b..0cc79ce1c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -12,6 +12,7 @@ namespace Barotrauma public override bool KeepDivingGearOn => true; public override bool IgnoreUnsafeHulls => true; public override bool ConcurrentObjectives => true; + public override bool AllowOutsideSubmarine => 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? @@ -26,14 +27,39 @@ namespace Barotrauma private AIObjectiveGoTo goToObjective; private AIObjectiveFindDivingGear divingGearObjective; - public AIObjectiveFindSafety(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { } + public AIObjectiveFindSafety(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { } protected override bool Check() => false; public override bool CanBeCompleted => true; private bool resetPriority; - public override float GetPriority() => Priority; + public override float GetPriority() + { + if (!IsAllowed) + { + Priority = 0; + return Priority; + } + if (character.CurrentHull == null) + { + Priority = objectiveManager.CurrentOrder is AIObjectiveGoTo && HumanAIController.HasDivingSuit(character) ? 0 : 100; + } + else + { + if (HumanAIController.NeedsDivingGear(character, character.CurrentHull, out _) && !HumanAIController.HasDivingGear(character)) + { + Priority = 100; + } + Priority = MathHelper.Clamp(Priority, 0, 100); + 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)); + } + } + return Priority; + } public override void Update(float deltaTime) { @@ -46,28 +72,20 @@ namespace Barotrauma if (character.CurrentHull == null) { currenthullSafety = 0; - Priority = objectiveManager.CurrentOrder is AIObjectiveGoTo ? 0 : 100; - return; - } - if (HumanAIController.NeedsDivingGear(character, character.CurrentHull, out _) && !HumanAIController.HasDivingGear(character)) - { - Priority = 100; - } - currenthullSafety = HumanAIController.CurrentHullSafety; - if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD) - { - Priority -= priorityDecrease * deltaTime; } else { - float dangerFactor = (100 - currenthullSafety) / 100; - Priority += dangerFactor * priorityIncrease * deltaTime; - } - Priority = MathHelper.Clamp(Priority, 0, 100); - 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)); + currenthullSafety = HumanAIController.CurrentHullSafety; + if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD) + { + Priority -= priorityDecrease * deltaTime; + } + else + { + float dangerFactor = (100 - currenthullSafety) / 100; + Priority += dangerFactor * priorityIncrease * deltaTime; + } + Priority = MathHelper.Clamp(Priority, 0, 100); } } @@ -76,34 +94,39 @@ namespace Barotrauma protected override void Act(float deltaTime) { var currentHull = character.CurrentHull; - bool needsDivingGear = HumanAIController.NeedsDivingGear(character, currentHull, out bool needsDivingSuit); - bool needsEquipment = false; - if (needsDivingSuit) + bool dangerousPressure = currentHull == null || currentHull.LethalPressure > 0; + if (!dangerousPressure) { - needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.lowOxygenThreshold); - } - else if (needsDivingGear) - { - needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.lowOxygenThreshold); - } - if (needsEquipment && divingGearObjective == null && !character.LockHands) - { - RemoveSubObjective(ref goToObjective); - TryAddSubObjective(ref divingGearObjective, - constructor: () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager), - onAbandon: () => - { - searchHullTimer = Math.Min(1, searchHullTimer); + // Don't try to seek diving gear if the pressure is dangerous. Just get out. + bool needsDivingGear = HumanAIController.NeedsDivingGear(character, currentHull, out bool needsDivingSuit); + bool needsEquipment = false; + if (needsDivingSuit) + { + needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.lowOxygenThreshold); + } + else if (needsDivingGear) + { + needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.lowOxygenThreshold); + } + if (needsEquipment && divingGearObjective == null && !character.LockHands) + { + 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); - }); + onCompleted: () => + { + resetPriority = true; + searchHullTimer = Math.Min(1, searchHullTimer); + RemoveSubObjective(ref divingGearObjective); + }); + } } - else if (divingGearObjective == null || !divingGearObjective.CanBeCompleted) + if (divingGearObjective == null || !divingGearObjective.CanBeCompleted) { if (currenthullSafety < HumanAIController.HULL_SAFETY_THRESHOLD) { @@ -128,14 +151,14 @@ namespace Barotrauma { RemoveSubObjective(ref goToObjective); } - TryAddSubObjective(ref goToObjective, + TryAddSubObjective(ref goToObjective, constructor: () => new AIObjectiveGoTo(currentSafeHull, character, objectiveManager, getDivingGearIfNeeded: true) { AllowGoingOutside = HumanAIController.HasDivingSuit(character, conditionPercentage: 50) }, onCompleted: () => { - if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD || + if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD || HumanAIController.NeedsDivingGear(character, currentHull, out bool needsSuit) && (needsSuit ? HumanAIController.HasDivingSuit(character) : HumanAIController.HasDivingMask(character))) { resetPriority = true; @@ -233,10 +256,8 @@ namespace Barotrauma //(no need to do the expensive pathfinding if we already know we're not going to choose this hull) if (hullSafety < bestValue) { continue; } // 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) + var path = PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition, nodeFilter: node => node.Waypoint.CurrentHull != null); + if (path.Unreachable) { HumanAIController.UnreachableHulls.Add(hull); continue; @@ -276,6 +297,7 @@ namespace Barotrauma float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, MathUtils.Pow(100000, 2), distance)); hullSafety *= distanceFactor; // If the target is not inside a friendly submarine, considerably reduce the hull safety. + // Intentionally exclude wrecks from this check if (hull.Submarine.TeamID != character.TeamID && hull.Submarine.TeamID != Character.TeamType.FriendlyNPC) { hullSafety /= 10; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index 4121ccdce..0ba7d3323 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -1,9 +1,9 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Linq; -using Barotrauma.Extensions; namespace Barotrauma { @@ -20,15 +20,23 @@ namespace Barotrauma private AIObjectiveGoTo gotoObjective; private AIObjectiveOperateItem operateObjective; - public AIObjectiveFixLeak(Gap leak, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base (character, objectiveManager, priorityModifier) + public bool IgnoreSeverityAndDistance { get; private set; } + + public AIObjectiveFixLeak(Gap leak, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool ignoreSeverityAndDistance = false) : base (character, objectiveManager, priorityModifier) { Leak = leak; + IgnoreSeverityAndDistance = ignoreSeverityAndDistance; } protected override bool Check() => Leak.Open <= 0 || Leak.Removed; public override float GetPriority() { + if (!IsAllowed) + { + Priority = 0; + return Priority; + } if (Leak.Removed || Leak.Open <= 0) { Priority = 0; @@ -39,8 +47,8 @@ namespace Barotrauma 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 distanceFactor = IgnoreSeverityAndDistance || xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, xDist + yDist * 3.0f)); + float severity = IgnoreSeverityAndDistance ? 1 : AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100; float max = Math.Min((AIObjectiveManager.OrderPriority - 1), 90); float devotion = CumulatedDevotion / 100; Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs index 659958632..995d71611 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs @@ -1,7 +1,5 @@ using Microsoft.Xna.Framework; -using System; using System.Linq; -using Barotrauma.Extensions; using System.Collections.Generic; namespace Barotrauma @@ -11,8 +9,12 @@ namespace Barotrauma public override string DebugTag => "fix leaks"; public override bool ForceRun => true; public override bool KeepDivingGearOn => true; + private Hull PrioritizedHull { get; set; } - public AIObjectiveFixLeaks(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { } + public AIObjectiveFixLeaks(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, Hull prioritizedHull = null) : base(character, objectiveManager, priorityModifier) + { + PrioritizedHull = prioritizedHull; + } protected override bool Filter(Gap gap) => IsValidTarget(gap, character); @@ -35,7 +37,7 @@ namespace Barotrauma protected override float TargetEvaluation() { - int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective(), onlyBots: true); + int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective() && !c.Character.IsIncapacitated, onlyBots: true); int totalLeaks = Targets.Count(); if (totalLeaks == 0) { return 0; } int secondaryLeaks = Targets.Count(l => l.IsRoomToRoom); @@ -60,7 +62,7 @@ namespace Barotrauma protected override IEnumerable GetList() => Gap.GapList; protected override AIObjective ObjectiveConstructor(Gap gap) - => new AIObjectiveFixLeak(gap, character, objectiveManager, PriorityModifier); + => new AIObjectiveFixLeak(gap, character, objectiveManager, priorityModifier: PriorityModifier, ignoreSeverityAndDistance: gap.FlowTargetHull == PrioritizedHull); protected override void OnObjectiveCompleted(AIObjective objective, Gap target) => HumanAIController.RemoveTargets(character, target); @@ -71,7 +73,11 @@ namespace Barotrauma if (gap.ConnectedWall == null || gap.ConnectedDoor != null || gap.Open <= 0 || gap.linkedTo.All(l => l == null)) { return false; } if (gap.Submarine == null) { return false; } if (gap.Submarine.TeamID != character.TeamID) { return false; } - if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(gap, true)) { return false; } + if (character.Submarine != null) + { + if (gap.Submarine.Info.Type != character.Submarine.Info.Type) { return false; } + if (!character.Submarine.IsEntityFoundOnThisSub(gap, true)) { return false; } + } return true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index 626c49064..3999a332d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -3,6 +3,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; +using Barotrauma.Extensions; namespace Barotrauma { @@ -20,7 +21,9 @@ namespace Barotrauma //can be either tags or identifiers private string[] itemIdentifiers; public IEnumerable Identifiers => itemIdentifiers; - private Item targetItem, moveToTarget, rootContainer; + + private Item targetItem; + private ISpatialEntity moveToTarget; private bool isDoneSeeking; public Item TargetItem => targetItem; private int currSearchIndex; @@ -29,6 +32,8 @@ namespace Barotrauma private float currItemPriority; private bool checkInventory; + public static float DefaultReach = 100; + public bool AllowToFindDivingGear { get; set; } = true; public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1) @@ -62,8 +67,7 @@ namespace Barotrauma if (item != null) { targetItem = item; - rootContainer = item.GetRootContainer(); - moveToTarget = rootContainer ?? item; + moveToTarget = item.GetRootInventoryOwner(); } return item != null; } @@ -86,6 +90,15 @@ namespace Barotrauma } if (!isDoneSeeking) { + bool dangerousPressure = character.CurrentHull == null || character.CurrentHull.LethalPressure > 0; + if (dangerousPressure) + { +#if DEBUG + DebugConsole.NewMessage($"{character.Name}: Seeking item aborted, because the pressure is dangerous.", Color.Yellow); +#endif + Abandon = true; + return; + } FindTargetItem(); objectiveManager.GetObjective().Wander(deltaTime); return; @@ -108,7 +121,26 @@ namespace Barotrauma Reset(); return; } - if (character.CanInteractWith(targetItem, out _, checkLinked: false)) + bool canInteract = false; + if (moveToTarget is Character c) + { + if (character == c) + { + canInteract = true; + moveToTarget = null; + } + else + { + character.SelectCharacter(c); + canInteract = character.CanInteractWith(c, maxDist: DefaultReach); + character.DeselectCharacter(); + } + } + else if (moveToTarget is Item parentItem) + { + canInteract = character.CanInteractWith(parentItem, out _, checkLinked: false); + } + if (canInteract) { var pickable = targetItem.GetComponent(); if (pickable == null) @@ -173,17 +205,17 @@ namespace Barotrauma } } } - else + else if (moveToTarget != null) { TryAddSubObjective(ref goToObjective, constructor: () => { - return new AIObjectiveGoTo(moveToTarget, character, objectiveManager, repeat: false, getDivingGearIfNeeded: AllowToFindDivingGear) + return new AIObjectiveGoTo(moveToTarget, character, objectiveManager, repeat: false, getDivingGearIfNeeded: AllowToFindDivingGear, closeEnough: DefaultReach) { // If the root container changes, the item is no longer where it was (taken by someone -> need to find another item) - abortCondition = () => targetItem == null || targetItem.GetRootContainer() != rootContainer, + abortCondition = () => targetItem == null || targetItem.GetRootInventoryOwner() != moveToTarget, DialogueIdentifier = "dialogcannotreachtarget", - TargetName = moveToTarget.Name + TargetName = (moveToTarget as MapEntity)?.Name ?? (moveToTarget as Character)?.Name ?? moveToTarget.ToString() }; }, onAbandon: () => @@ -212,23 +244,27 @@ namespace Barotrauma { currSearchIndex++; var item = Item.ItemList[currSearchIndex]; - if (item.Submarine == null) { continue; } - if (item.CurrentHull == null) { continue; } - if (item.Submarine.TeamID != character.TeamID) { continue; } + Submarine itemSub = item.Submarine ?? item.ParentInventory?.Owner?.Submarine; + if (itemSub == null) { continue; } + if (itemSub.TeamID != character.TeamID) { continue; } if (!CheckItem(item)) { continue; } if (ignoredContainerIdentifiers != null && item.Container != null) { if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; } } - if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { continue; } + if (character.Submarine != null) + { + if (itemSub.Info.Type != character.Submarine.Info.Type) { continue; } + if (character.Submarine.GetConnectedSubs().None(s => s == itemSub && itemSub.TeamID == character.TeamID && itemSub.Info.Type == character.Submarine.Info.Type)) { continue; } + } if (character.IsItemTakenBySomeoneElse(item)) { continue; } float itemPriority = 1; if (GetItemPriority != null) { itemPriority = GetItemPriority(item); } - Item rootContainer = item.GetRootContainer(); - Vector2 itemPos = (rootContainer ?? item).WorldPosition; + Entity rootInventoryOwner = item.GetRootInventoryOwner(); + Vector2 itemPos = (rootInventoryOwner ?? 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; @@ -239,8 +275,7 @@ namespace Barotrauma if (itemPriority < currItemPriority) { continue; } currItemPriority = itemPriority; targetItem = item; - moveToTarget = rootContainer ?? item; - this.rootContainer = rootContainer; + moveToTarget = rootInventoryOwner ?? item; } if (currSearchIndex >= Item.ItemList.Count - 1) { @@ -276,6 +311,7 @@ namespace Barotrauma private bool CheckItem(Item item) { + if (item.NonInteractable) { return false; } if (ignoredItems.Contains(item)) { return false; }; if (item.Condition < TargetCondition) { return false; } if (ItemFilter != null && !ItemFilter(item)) { return false; } @@ -288,7 +324,6 @@ namespace Barotrauma RemoveSubObjective(ref goToObjective); targetItem = null; moveToTarget = null; - rootContainer = null; isDoneSeeking = false; currSearchIndex = 0; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index 844343053..43319c633 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -44,6 +44,9 @@ namespace Barotrauma public bool AllowGoingOutside { get; set; } public override bool AbandonWhenCannotCompleteSubjectives => !repeat; + + public override bool AllowOutsideSubmarine => AllowGoingOutside; + public string DialogueIdentifier { get; set; } public string TargetName { get; set; } @@ -63,31 +66,42 @@ namespace Barotrauma { Priority = 0; } - return objectiveManager.CurrentOrder == this ? AIObjectiveManager.OrderPriority : Priority; + else + { + Priority = objectiveManager.CurrentOrder == this ? AIObjectiveManager.OrderPriority : 10; + } + return Priority; } - public AIObjectiveGoTo(ISpatialEntity target, Character character, AIObjectiveManager objectiveManager, bool repeat = false, bool getDivingGearIfNeeded = true, float priorityModifier = 1, float closeEnough = 0) - : base (character, objectiveManager, priorityModifier) + 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; - CloseEnough = closeEnough; if (Target is Item i) { CloseEnough = Math.Max(CloseEnough, i.InteractDistance + Math.Max(i.Rect.Width, i.Rect.Height) / 2); } + else if (Target is Character) + { + CloseEnough = Math.Max(closeEnough, AIObjectiveGetItem.DefaultReach); + } + else + { + CloseEnough = closeEnough; + } } private void SpeakCannotReach() { #if DEBUG - DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target.ToString()}", Color.Yellow); + DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target}", Color.Yellow); #endif if (objectiveManager.CurrentOrder != null && DialogueIdentifier != null) { - string msg = TargetName == null ? TextManager.Get(DialogueIdentifier, true) : TextManager.GetWithVariable(DialogueIdentifier, "[name]", TargetName, true); + string msg = TargetName == null ? TextManager.Get(DialogueIdentifier, true) : TextManager.GetWithVariable(DialogueIdentifier, "[name]", TargetName, formatCapitals: !(Target is Character)); if (msg != null) { character.Speak(msg, identifier: DialogueIdentifier, minDurationBetweenSimilar: 20.0f); @@ -200,16 +214,25 @@ namespace Barotrauma } 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) + if (repeat) { - OnCompleted(); - return; + if (IsCloseEnough) + { + if (requiredCondition == null || requiredCondition()) + { + if (character.CanSeeTarget(Target)) + { + OnCompleted(); + return; + } + } + } } if (SteeringManager == PathSteering) { @@ -237,7 +260,7 @@ namespace Barotrauma } } - private Hull GetTargetHull() + public Hull GetTargetHull() { if (Target is Hull h) { @@ -277,13 +300,7 @@ namespace Barotrauma //otherwise characters can let go of the ladders too soon once they're close enough to the target if (PathSteering.CurrentPath.NextNode != null) { return false; } } - - bool closeEnough = Vector2.DistanceSquared(Target.WorldPosition, character.WorldPosition) < CloseEnough * CloseEnough; - if (closeEnough) - { - closeEnough = !(Target is Character) || Target is Character c && c.CurrentHull == character.CurrentHull; - } - return closeEnough; + return Vector2.DistanceSquared(Target.WorldPosition, character.WorldPosition) < CloseEnough * CloseEnough; } } @@ -319,7 +336,9 @@ namespace Barotrauma } else if (Target is Character targetCharacter) { - if (character.CanInteractWith(targetCharacter, CloseEnough)) { IsCompleted = true; } + character.SelectCharacter(targetCharacter); + if (character.CanInteractWith(targetCharacter, skipDistanceCheck: true)) { IsCompleted = true; } + character.DeselectCharacter(); } else { @@ -331,6 +350,16 @@ namespace Barotrauma return IsCompleted; } + protected override void OnAbandon() + { + StopMovement(); + if (SteeringManager == PathSteering) + { + PathSteering.ResetPath(); + } + base.OnAbandon(); + } + private void StopMovement() { character.AIController.SteeringManager.Reset(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index 0d9a61f81..ba35ef7a1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -3,7 +3,6 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using Barotrauma.Extensions; namespace Barotrauma { @@ -11,6 +10,7 @@ namespace Barotrauma { public override string DebugTag => "idle"; public override bool UnequipItems => true; + public override bool AllowOutsideSubmarine => true; private readonly float newTargetIntervalMin = 10; private readonly float newTargetIntervalMax = 20; @@ -34,41 +34,43 @@ namespace Barotrauma { standStillTimer = Rand.Range(-10.0f, 10.0f); walkDuration = Rand.Range(0.0f, 10.0f); + CalculatePriority(); } 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); } + public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace); } private float randomTimer; private float randomUpdateInterval = 5; public float Random { get; private set; } - public void CalculatePriority() + public void CalculatePriority(float max = 0) { - Random = Rand.Range(0.5f, 1.5f); - randomTimer = randomUpdateInterval; - float max = Math.Min(Math.Min(AIObjectiveManager.RunPriority, AIObjectiveManager.OrderPriority) - 1, 100); - float initiative = character.GetSkillLevel("initiative"); - Priority = MathHelper.Lerp(1, max, MathUtils.InverseLerp(100, 0, initiative * Random)); + //Random = Rand.Range(0.5f, 1.5f); + //randomTimer = randomUpdateInterval; + //max = max > 0 ? max : Math.Min(Math.Min(AIObjectiveManager.RunPriority, AIObjectiveManager.OrderPriority) - 1, 100); + //float initiative = character.GetSkillLevel("initiative"); + //Priority = MathHelper.Lerp(1, max, MathUtils.InverseLerp(100, 0, initiative * Random)); + Priority = 1; } public override float GetPriority() => Priority; public override void Update(float deltaTime) { - if (objectiveManager.CurrentObjective == this) - { - if (randomTimer > 0) - { - randomTimer -= deltaTime; - } - else - { - CalculatePriority(); - } - } + //if (objectiveManager.CurrentObjective == this) + //{ + // if (randomTimer > 0) + // { + // randomTimer -= deltaTime; + // } + // else + // { + // CalculatePriority(); + // } + //} } protected override void Act(float deltaTime) @@ -128,7 +130,7 @@ namespace Barotrauma //choose a random available hull currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced); bool isCurrentHullAllowed = !IsForbidden(character.CurrentHull); - var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, nodeFilter: node => + var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: $"AIObjectiveIdle {character.DisplayName}", nodeFilter: node => { if (node.Waypoint.CurrentHull == null) { return false; } // Check that there is no unsafe or forbidden hulls on the way to the target @@ -231,9 +233,11 @@ namespace Barotrauma { if (HumanAIController.UnsafeHulls.Contains(hull)) { continue; } if (hull.Submarine == null) { continue; } - if (hull.Submarine.TeamID != character.TeamID) { continue; } - // If the character is inside, only take connected hulls into account. - if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(hull, true)) { continue; } + if (character.Submarine == null) { break; } + if (hull.Submarine.TeamID != character.Submarine.TeamID) { continue; } + if (hull.Submarine.Info.Type != character.Submarine.Info.Type) { continue; } + // If the character is inside, only take connected subs into account. + if (!character.Submarine.IsEntityFoundOnThisSub(hull, true)) { continue; } if (IsForbidden(hull)) { continue; } // Ignore hulls that are too low to stand inside if (character.AnimController is HumanoidAnimController animController) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs index 8c2a4b340..7d51adb9f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs @@ -47,7 +47,7 @@ namespace Barotrauma public override bool AllowSubObjectiveSorting => true; public virtual bool InverseTargetEvaluation => false; - public override bool IsLoop { get => true; set => throw new System.Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace); } + public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace); } public override void Update(float deltaTime) { @@ -108,6 +108,11 @@ namespace Barotrauma public override float GetPriority() { + if (!IsAllowed) + { + Priority = 0; + return Priority; + } if (character.LockHands || character.Submarine == null || Targets.None()) { Priority = 0; @@ -199,7 +204,7 @@ namespace Barotrauma { Objectives.Remove(target); ignoreList.Add(target); - targetUpdateTimer = 0; + targetUpdateTimer = Math.Min(0.1f, targetUpdateTimer); }; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index abe0f4052..698b777d7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -1,9 +1,10 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using Barotrauma.Extensions; -using Microsoft.Xna.Framework; namespace Barotrauma { @@ -13,8 +14,11 @@ 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 = 3; + public const float baseDevotion = 5; + /// + /// Excluding the current order. + /// public List Objectives { get; private set; } = new List(); private readonly Character character; @@ -87,8 +91,25 @@ namespace Barotrauma public Dictionary DelayedObjectives { get; private set; } = new Dictionary(); + private void ClearIgnored() + { + if (character.AIController is HumanAIController humanAi) + { + humanAi.UnreachableHulls.Clear(); + humanAi.IgnoredItems.Clear(); + } + } + public void CreateAutonomousObjectives() { + if (character.IsDead) + { +#if DEBUG + DebugConsole.ThrowError("Attempted to create autonomous orders for a dead character"); +#else + return; +#endif + } foreach (var delayedObjective in DelayedObjectives) { CoroutineManager.StopCoroutines(delayedObjective.Value); @@ -98,24 +119,16 @@ namespace Barotrauma AddObjective(new AIObjectiveFindSafety(character, this)); AddObjective(new AIObjectiveIdle(character, this)); int objectiveCount = Objectives.Count; - foreach (var automaticOrder in character.Info.Job.Prefab.AutomaticOrders) + foreach (var autonomousObjective in character.Info.Job.Prefab.AutonomousObjective) { - var orderPrefab = Order.GetPrefab(automaticOrder.identifier); - if (orderPrefab == null) { throw new Exception($"Could not find a matching prefab by the identifier: '{automaticOrder.identifier}'"); } - // TODO: Similar code is used in CrewManager:815-> DRY - var matchingItems = orderPrefab.ItemIdentifiers.Any() ? - Item.ItemList.FindAll(it => orderPrefab.ItemIdentifiers.Contains(it.Prefab.Identifier) || it.HasTag(orderPrefab.ItemIdentifiers)) : - Item.ItemList.FindAll(it => it.Components.Any(ic => ic.GetType() == orderPrefab.ItemComponentType)); - matchingItems.RemoveAll(it => it.Submarine != character.Submarine); - var item = matchingItems.GetRandom(); - var order = new Order( - orderPrefab, - item ?? character.CurrentHull as Entity, - item?.Components.FirstOrDefault(ic => ic.GetType() == orderPrefab.ItemComponentType), - orderGiver: character); + var orderPrefab = Order.GetPrefab(autonomousObjective.identifier); + if (orderPrefab == null) { throw new Exception($"Could not find a matching prefab by the identifier: '{autonomousObjective.identifier}'"); } + var item = orderPrefab.MustSetTarget ? orderPrefab.GetMatchingItems(character.Submarine, false)?.GetRandom() : null; + var order = new Order(orderPrefab, item ?? character.CurrentHull as Entity, + item?.Components.FirstOrDefault(ic => ic.GetType() == orderPrefab.ItemComponentType), orderGiver: character); if (order == null) { continue; } - var objective = CreateObjective(order, automaticOrder.option, character, automaticOrder.priorityModifier); - if (objective != null) + var objective = CreateObjective(order, autonomousObjective.option, character, isAutonomous: true, autonomousObjective.priorityModifier); + if (objective != null && objective.CanBeCompleted) { AddObjective(objective, delay: Rand.Value() / 2); objectiveCount++; @@ -167,7 +180,7 @@ namespace Barotrauma { previousObjective?.OnDeselected(); CurrentObjective?.OnSelected(); - GetObjective().CalculatePriority(); + GetObjective().CalculatePriority(Math.Max(CurrentObjective.Priority - 10, 0)); } return CurrentObjective; } @@ -179,7 +192,27 @@ namespace Barotrauma public void UpdateObjectives(float deltaTime) { - CurrentOrder?.Update(deltaTime); + if (CurrentOrder != null) + { + if (CurrentOrder.IsCompleted) + { +#if DEBUG + DebugConsole.NewMessage($"{character.Name}: Removing order {CurrentOrder.DebugTag}, because it is completed.", Color.LightGreen); +#endif + CurrentOrder = null; + } + else if (!CurrentOrder.CanBeCompleted) + { +#if DEBUG + DebugConsole.NewMessage($"{character.Name}: Removing order {CurrentOrder.DebugTag}, because it cannot be completed.", Color.Red); +#endif + CurrentOrder = null; + } + else + { + CurrentOrder.Update(deltaTime); + } + } if (WaitTimer > 0) { WaitTimer -= deltaTime; @@ -202,7 +235,7 @@ namespace Barotrauma #endif Objectives.Remove(objective); } - else if (objective != CurrentOrder) + else { objective.Update(deltaTime); } @@ -212,14 +245,15 @@ namespace Barotrauma public void SortObjectives() { + CurrentOrder?.GetPriority(); + Objectives.ForEach(o => o.GetPriority()); if (Objectives.Any()) { - Objectives.ForEach(o => o.GetPriority()); Objectives.Sort((x, y) => y.Priority.CompareTo(x.Priority)); } GetCurrentObjective()?.SortSubObjectives(); } - + public void DoCurrentObjective(float deltaTime) { if (WaitTimer <= 0) @@ -231,7 +265,7 @@ namespace Barotrauma character.AIController.SteeringManager.Reset(); } } - + public void SetOrder(AIObjective objective) { CurrentOrder = objective; @@ -239,7 +273,16 @@ namespace Barotrauma public void SetOrder(Order order, string option, Character orderGiver) { - CurrentOrder = CreateObjective(order, option, orderGiver); + if (character.IsDead) + { +#if DEBUG + DebugConsole.ThrowError("Attempted to set an order for a dead character"); +#else + return; +#endif + } + ClearIgnored(); + CurrentOrder = CreateObjective(order, option, orderGiver, isAutonomous: false); if (CurrentOrder == null) { // Recreate objectives, because some of them may be removed, if impossible to complete (e.g. due to path finding) @@ -251,7 +294,7 @@ namespace Barotrauma } } - public AIObjective CreateObjective(Order order, string option, Character orderGiver, float priorityModifier = 1) + public AIObjective CreateObjective(Order order, string option, Character orderGiver, bool isAutonomous, float priorityModifier = 1) { if (order == null) { return null; } AIObjective newObjective; @@ -270,13 +313,13 @@ namespace Barotrauma }; break; case "wait": - newObjective = new AIObjectiveGoTo(character, character, this, repeat: true, priorityModifier: priorityModifier) + newObjective = new AIObjectiveGoTo(order.TargetEntity ?? character, character, this, repeat: true, priorityModifier: priorityModifier) { AllowGoingOutside = character.CurrentHull == null }; break; case "fixleaks": - newObjective = new AIObjectiveFixLeaks(character, this, priorityModifier); + newObjective = new AIObjectiveFixLeaks(character, this, priorityModifier: priorityModifier, prioritizedHull: order.TargetEntity as Hull); break; case "chargebatteries": newObjective = new AIObjectiveChargeBatteries(character, this, option, priorityModifier); @@ -285,13 +328,31 @@ namespace Barotrauma newObjective = new AIObjectiveRescueAll(character, this, priorityModifier); break; case "repairsystems": - newObjective = new AIObjectiveRepairItems(character, this, priorityModifier) + case "repairmechanical": + case "repairelectrical": + newObjective = new AIObjectiveRepairItems(character, this, priorityModifier: priorityModifier, prioritizedItem: order.TargetEntity as Item) { - RequireAdequateSkills = option == "jobspecific" + RelevantSkill = order.AppropriateSkill, + RequireAdequateSkills = isAutonomous }; break; case "pumpwater": - newObjective = new AIObjectivePumpWater(character, this, option, priorityModifier: priorityModifier); + if (order.TargetItemComponent is Pump targetPump) + { + if (order.TargetItemComponent.Item.NonInteractable) { return null; } + newObjective = new AIObjectiveOperateItem(targetPump, character, this, option, false, priorityModifier: priorityModifier) + { + IsLoop = true, + Override = orderGiver != null && orderGiver.IsPlayer + }; + // ItemComponent.AIOperate() returns false by default -> We'd have to set IsLoop = false and implement a custom override of AIOperate for the Pump.cs, + // if we want that the bot just switches the pump on/off and continues doing something else. + // If we want that the bot does the objective and then forgets about it, I think we could do the same plus dismiss when the bot is done. + } + else + { + newObjective = new AIObjectivePumpWater(character, this, option, priorityModifier: priorityModifier); + } break; case "extinguishfires": newObjective = new AIObjectiveExtinguishFires(character, this, priorityModifier); @@ -301,9 +362,11 @@ namespace Barotrauma break; case "steer": var steering = (order?.TargetEntity as Item)?.GetComponent(); - if (steering != null) steering.PosToMaintain = steering.Item.Submarine?.WorldPosition; + 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) + if (order.TargetItemComponent.Item.NonInteractable) { return null; } + newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, + requireEquip: false, useController: order.UseController, controller: order.ConnectedController, priorityModifier: priorityModifier) { IsLoop = true, // Don't override unless it's an order by a player @@ -312,17 +375,32 @@ namespace Barotrauma break; default: if (order.TargetItemComponent == null) { return null; } - newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, requireEquip: false, useController: order.UseController, priorityModifier: priorityModifier) + if (order.TargetItemComponent.Item.NonInteractable) { return null; } + newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, option, + requireEquip: false, useController: order.UseController, controller: order.ConnectedController, priorityModifier: priorityModifier) { - IsLoop = true, + IsLoop = option != "shutdown", // Don't override unless it's an order by a player Override = orderGiver != null && orderGiver.IsPlayer }; + if (newObjective.Abandon) { return null; } break; } return newObjective; } + private void DismissSelf() + { +#if CLIENT + if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer) + { + GameMain.GameSession?.CrewManager?.SetCharacterOrder(character, Order.GetPrefab("dismissed"), null, character); + } +#else + GameMain.Server?.SendOrderChatMessage(new OrderChatMessage(Order.GetPrefab("dismissed"), null, null, character, character)); +#endif + } + private bool IsAllowedToWait() { if (CurrentOrder != null) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs index 62dae935b..bd00d7734 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -3,7 +3,6 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using Barotrauma.Extensions; namespace Barotrauma { @@ -32,6 +31,11 @@ namespace Barotrauma public override float GetPriority() { + if (!IsAllowed) + { + Priority = 0; + return Priority; + } if (component.Item.ConditionPercentage <= 0) { Priority = 0; @@ -42,38 +46,74 @@ namespace Barotrauma { Priority = AIObjectiveManager.OrderPriority; } - - if (component.Item.CurrentHull == null || component.Item.CurrentHull.FireSources.Any() || HumanAIController.IsItemOperatedByAnother(GetTarget(), out _)) + ItemComponent target = GetTarget(); + Item targetItem = target?.Item; + if (targetItem == null) + { +#if DEBUG + DebugConsole.ThrowError("Item or component of AI Objective Operate item wass null. This shouldn't happen."); +#endif + Abandon = true; + Priority = 0; + return Priority; + } + switch (Option) + { + case "shutdown": + var powered = component?.Item.GetComponent(); + if (powered != null && powered.IsActive) + { + Priority = 0; + return Priority; + } + break; + case "powerup": + // Check that we don't already have another order that is targeting the same item. + if (objectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder != this && operateOrder.GetTarget() == target) + { + Priority = 0; + return Priority; + } + break; + } + if (targetItem.CurrentHull == null || targetItem.CurrentHull.FireSources.Any() || HumanAIController.IsItemOperatedByAnother(target, out _)) { Priority = 0; } - else if (Character.CharacterList.Any(c => c.CurrentHull == component.Item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) + else if (Character.CharacterList.Any(c => c.CurrentHull == targetItem.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { Priority = 0; } else { float value = CumulatedDevotion + (AIObjectiveManager.OrderPriority * PriorityModifier); - float max = MathHelper.Min((AIObjectiveManager.OrderPriority - 1), 90); + float max = objectiveManager.CurrentOrder == this ? MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90) : AIObjectiveManager.RunPriority - 1; Priority = MathHelper.Clamp(value, 0, max); } } return Priority; } - public AIObjectiveOperateItem(ItemComponent item, Character character, AIObjectiveManager objectiveManager, string option, bool requireEquip, Entity operateTarget = null, bool useController = false, float priorityModifier = 1) - : base (character, objectiveManager, priorityModifier, option) + public AIObjectiveOperateItem(ItemComponent item, Character character, AIObjectiveManager objectiveManager, string option, bool requireEquip, + Entity operateTarget = null, bool useController = false, ItemComponent controller = null, float priorityModifier = 1) + : base(character, objectiveManager, priorityModifier, option) { - this.component = item ?? throw new System.ArgumentNullException("item", "Attempted to create an AIObjectiveOperateItem with a null target."); + component = item ?? throw new ArgumentNullException("item", "Attempted to create an AIObjectiveOperateItem with a null target."); this.requireEquip = requireEquip; this.operateTarget = operateTarget; this.useController = useController; - if (useController) + if (useController) { this.controller = controller ?? component?.Item?.FindController(); } + var target = GetTarget(); + if (target == null) { - //try finding the controller with the simpler non-recursive method first - controller = - component.Item.GetConnectedComponents().FirstOrDefault() ?? - component.Item.GetConnectedComponents(recursive: true).FirstOrDefault(); +#if DEBUG + throw new Exception("target null"); +#endif + Abandon = true; + } + else if (target.Item.NonInteractable) + { + Abandon = true; } } @@ -117,7 +157,7 @@ namespace Barotrauma { DialogueIdentifier = "dialogcannotreachtarget", TargetName = target.Item.Name - }, + }, onAbandon: () => Abandon = true, onCompleted: () => RemoveSubObjective(ref goToObjective)); } @@ -132,7 +172,7 @@ namespace Barotrauma } 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)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs index 0921dc1ff..a7fe83d4b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.Linq; using Microsoft.Xna.Framework; -using Barotrauma.Extensions; namespace Barotrauma { @@ -27,13 +26,18 @@ namespace Barotrauma protected override bool Filter(Pump pump) { if (pump == null) { return false; } + if (pump.Item.NonInteractable) { return false; } if (pump.Item.HasTag("ballast")) { return false; } if (pump.Item.Submarine == null) { return false; } if (pump.Item.CurrentHull == null) { return false; } if (pump.Item.Submarine.TeamID != character.TeamID) { return false; } 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.Submarine != null) + { + if (pump.Item.Submarine.Info.Type != character.Submarine.Info.Type) { return false; } + if (!character.Submarine.IsEntityFoundOnThisSub(pump.Item, true)) { 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; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index 1e5971576..3011e173e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -3,7 +3,6 @@ using Microsoft.Xna.Framework; using System; using System.Linq; using Barotrauma.Extensions; -using FarseerPhysics; namespace Barotrauma { @@ -20,14 +19,22 @@ namespace Barotrauma private RepairTool repairTool; private bool IsRepairing => character.SelectedConstruction == Item && Item.GetComponent()?.CurrentFixer == character; + private readonly bool isPriority; - public AIObjectiveRepairItem(Character character, Item item, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) + public AIObjectiveRepairItem(Character character, Item item, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool isPriority = false) + : base(character, objectiveManager, priorityModifier) { Item = item; + this.isPriority = isPriority; } public override float GetPriority() { + if (!IsAllowed) + { + Priority = 0; + return Priority; + } // TODO: priority list? // Ignore items that are being repaired by someone else. if (Item.Repairables.Any(r => r.CurrentFixer != null && r.CurrentFixer != character)) @@ -36,20 +43,19 @@ namespace Barotrauma } else { - 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) + float distanceFactor = 1; + if (!isPriority && Item.CurrentHull != character.CurrentHull) { - distanceFactor = 1; + 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; + distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 5000, dist)); } - float damagePriority = MathHelper.Lerp(1, 0, Item.Condition / Item.MaxCondition); - float successFactor = MathHelper.Lerp(0, 1, Item.Repairables.Average(r => r.DegreeOfSuccess(character))); + float severity = isPriority ? 1 : AIObjectiveRepairItems.GetTargetPriority(Item, character); float isSelected = IsRepairing ? 50 : 0; float devotion = (CumulatedDevotion + isSelected) / 100; float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90); - Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (damagePriority * distanceFactor * successFactor * PriorityModifier), 0, 1)); + Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1)); } return Priority; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs index f6df24810..e9287f01f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using Barotrauma.Items.Components; using Barotrauma.Extensions; +using Microsoft.Xna.Framework; namespace Barotrauma { @@ -15,12 +16,23 @@ namespace Barotrauma /// public bool RequireAdequateSkills; + /// + /// If set, only fix items where required skill matches this. + /// + public string RelevantSkill; + + private readonly Item prioritizedItem; + public override bool AllowMultipleInstances => true; 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) { } + public AIObjectiveRepairItems(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, Item prioritizedItem = null) + : base(character, objectiveManager, priorityModifier) + { + this.prioritizedItem = prioritizedItem; + } protected override void CreateObjectives() { @@ -45,7 +57,7 @@ namespace Barotrauma { Objectives.Remove(item); ignoreList.Add(item); - targetUpdateTimer = 0; + targetUpdateTimer = Math.Min(0.1f, targetUpdateTimer); }; } break; @@ -67,9 +79,9 @@ namespace Barotrauma if (item.Repairables.All(r => condition >= r.AIRepairThreshold)) { return false; } } } - if (RequireAdequateSkills) + if (!string.IsNullOrWhiteSpace(RelevantSkill)) { - if (item.Repairables.Any(r => !r.HasRequiredSkills(character))) { return false; } + if (item.Repairables.None(r => r.requiredSkills.Any(s => s.Identifier.Equals(RelevantSkill, StringComparison.OrdinalIgnoreCase)))) { return false; } } return true; } @@ -81,7 +93,7 @@ namespace Barotrauma // Don't stop fixing until done return 100; } - int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective(), onlyBots: true); + int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective() && !c.Character.IsIncapacitated, onlyBots: true); int items = Targets.Count; bool anyFixers = otherFixers > 0; float ratio = anyFixers ? items / (float)otherFixers : 1; @@ -96,14 +108,28 @@ namespace Barotrauma // Enough fixers return 0; } - return Targets.Sum(t => 100 - t.ConditionPercentage) * ratio; + if (RequireAdequateSkills) + { + return Targets.Sum(t => GetTargetPriority(t, character)) * ratio; + } + else + { + return Targets.Sum(t => 100 - t.ConditionPercentage) * ratio; + } } } + public static float GetTargetPriority(Item item, Character character) + { + float damagePriority = MathHelper.Lerp(1, 0, item.Condition / item.MaxCondition); + float successFactor = MathHelper.Lerp(0, 1, item.Repairables.Average(r => r.DegreeOfSuccess(character))); + return MathHelper.Lerp(0, 100, MathHelper.Clamp(damagePriority * successFactor, 0, 1)); + } + protected override IEnumerable GetList() => Item.ItemList; protected override AIObjective ObjectiveConstructor(Item item) - => new AIObjectiveRepairItem(character, item, objectiveManager, PriorityModifier); + => new AIObjectiveRepairItem(character, item, objectiveManager, priorityModifier: PriorityModifier, isPriority: item == prioritizedItem); protected override void OnObjectiveCompleted(AIObjective objective, Item target) => HumanAIController.RemoveTargets(character, target); @@ -111,12 +137,17 @@ namespace Barotrauma public static bool IsValidTarget(Item item, Character character) { if (item == null) { return false; } + if (item.NonInteractable) { return false; } if (item.IsFullCondition) { return false; } if (item.CurrentHull == null) { return false; } if (item.Submarine == null) { return false; } if (item.Submarine.TeamID != character.TeamID) { return false; } if (item.Repairables.None()) { return false; } - if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { return false; } + if (character.Submarine != null) + { + if (item.Submarine.Info.Type != character.Submarine.Info.Type) { return false; } + if (!character.Submarine.IsEntityFoundOnThisSub(item, true)) { return false; } + } return true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index a44ce0ed8..87f72d5ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -38,7 +38,19 @@ namespace Barotrauma } this.targetCharacter = targetCharacter; } - + + protected override void OnAbandon() + { + character.SelectedCharacter = null; + base.OnAbandon(); + } + + protected override void OnCompleted() + { + character.SelectedCharacter = null; + base.OnCompleted(); + } + protected override void Act(float deltaTime) { if (character.LockHands || targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead) @@ -46,16 +58,13 @@ namespace Barotrauma Abandon = true; return; } - if (targetCharacter.SelectedBy != null && targetCharacter.SelectedBy != character) + var otherRescuer = targetCharacter.SelectedBy; + if (otherRescuer != null && otherRescuer != character) { - var otherCharacter = character.SelectedBy; - if (otherCharacter != null) - { - // Someone else is rescuing/holding the target. - Abandon = otherCharacter.IsPlayer || character.GetSkillLevel("medical") < otherCharacter.GetSkillLevel("medical"); - } + // Someone else is rescuing/holding the target. + Abandon = otherRescuer.IsPlayer || character.GetSkillLevel("medical") < otherRescuer.GetSkillLevel("medical"); + return; } - if (targetCharacter != character) { // Incapacitated target is not in a safe place -> Move to a safe place first @@ -63,9 +72,12 @@ namespace Barotrauma { 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); + if (targetCharacter.CurrentHull.DisplayName != null) + { + 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)) @@ -142,10 +154,13 @@ namespace Barotrauma { // We can start applying treatment 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 }), - null, 1.0f, "foundwoundedtarget" + targetCharacter.Name, 60.0f); + { + if (targetCharacter.CurrentHull.DisplayName != null) + { + character.Speak(TextManager.GetWithVariables("DialogFoundWoundedTarget", new string[2] { "[targetname]", "[roomname]" }, + new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }), + null, 1.0f, "foundwoundedtarget" + targetCharacter.Name, 60.0f); + } character.SelectCharacter(targetCharacter); } @@ -155,13 +170,23 @@ namespace Barotrauma private readonly List suitableItemIdentifiers = new List(); private readonly List itemNameList = new List(); - private Dictionary currentTreatmentSuitabilities = new Dictionary(); + private readonly Dictionary currentTreatmentSuitabilities = new Dictionary(); private void GiveTreatment(float deltaTime) { + if (targetCharacter == null) + { + string errorMsg = $"{character.Name}: Attempted to update a Rescue objective with no target!"; + DebugConsole.ThrowError(errorMsg); + Abandon = true; + return; + } + + SteeringManager?.Reset(); + if (!targetCharacter.IsPlayer) { // If the target is a bot, don't let it move - targetCharacter.AIController?.SteeringManager.Reset(); + targetCharacter.AIController?.SteeringManager?.Reset(); } if (treatmentTimer > 0.0f) { @@ -176,6 +201,8 @@ namespace Barotrauma //check if we already have a suitable treatment for any of the afflictions foreach (Affliction affliction in GetSortedAfflictions(targetCharacter)) { + if (affliction == null) { throw new Exception("Affliction was null"); } + if (affliction.Prefab == null) { throw new Exception("Affliction prefab was null"); } foreach (KeyValuePair treatmentSuitability in affliction.Prefab.TreatmentSuitability) { if (currentTreatmentSuitabilities.ContainsKey(treatmentSuitability.Key) && currentTreatmentSuitabilities[treatmentSuitability.Key] > 0.0f) @@ -288,6 +315,11 @@ namespace Barotrauma public override float GetPriority() { + if (!IsAllowed) + { + Priority = 0; + return Priority; + } if (targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead) { Priority = 0; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs index d3d9adfed..e01280b2f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs @@ -33,46 +33,32 @@ namespace Barotrauma protected override float TargetEvaluation() { - int otherRescuers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective(), onlyBots: true); - int targetCount = Targets.Count; - bool anyRescuers = otherRescuers > 0; - float ratio = anyRescuers ? targetCount / (float)otherRescuers : 1; - if (objectiveManager.CurrentOrder == this) + if (objectiveManager.CurrentOrder != this) { - return Targets.Min(t => GetVitalityFactor(t)) / ratio; - } - else - { - float multiplier = 1; - if (anyRescuers) + if (!character.IsMedic && HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.IsMedic && !c.Character.IsUnconscious)) { - float mySkill = character.GetSkillLevel("medical"); - int betterRescuers = HumanAIController.CountCrew(c => c != HumanAIController && c.Character.Info.Job.GetSkillLevel("medical") >= mySkill, onlyBots: true); - if (targetCount / (float)betterRescuers <= 1) - { - // Enough rescuers - return 100; - } - else - { - bool foundOtherMedics = HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.Info.Job.Prefab.Identifier == "medicaldoctor"); - if (foundOtherMedics) - { - if (character.Info.Job.Prefab.Identifier != "medicaldoctor") - { - // Double the vitality factor -> less likely to take action - multiplier = 2; - } - } - } + // Don't do anything if there's a medic on board and we are not a medic + return 100; } - return Targets.Min(t => GetVitalityFactor(t)) / ratio * multiplier; } + float worstCondition = Targets.Min(t => GetVitalityFactor(t)); + if (Targets.Contains(character)) + { + if (character.Bleeding > 10) + { + // Enforce the highest priority when bleeding out. + worstCondition = 0; + } + // Boost the priority when wounded. + worstCondition /= 2; + } + return worstCondition; } public static float GetVitalityFactor(Character character) { - float vitality = character.HealthPercentage - character.Bleeding - character.Bloodloss + Math.Min(character.Oxygen, 0); + float vitality = character.HealthPercentage - (character.Bleeding * 2) - character.Bloodloss + Math.Min(character.Oxygen, 0); + vitality -= character.CharacterHealth.GetAfflictionStrength("paralysis"); return Math.Clamp(vitality, 0, 100); } @@ -91,6 +77,11 @@ namespace Barotrauma if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target)) { return false; } if (!humanAI.ObjectiveManager.IsCurrentOrder()) { + if (!character.IsMedic && target != character) + { + // Don't allow to treat others autonomously + return false; + } // Ignore unsafe hulls, unless ordered if (humanAI.UnsafeHulls.Contains(target.CurrentHull)) { @@ -105,11 +96,15 @@ namespace Barotrauma 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; } - if (!target.IsPlayer && HumanAIController.IsActive(target) && target.AIController is HumanAIController targetAI) + if (character.Submarine != null) + { + if (target.Submarine.Info.Type != character.Submarine.Info.Type) { return false; } + if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, true)) { return false; } + } + if (target != character &&!target.IsPlayer && HumanAIController.IsActive(target) && target.AIController is HumanAIController targetAI) { // Ignore all concious targets that are currently fighting, fleeing or treating characters - if (targetAI.ObjectiveManager.HasActiveObjective() || + if (targetAI.ObjectiveManager.HasActiveObjective() || targetAI.ObjectiveManager.HasActiveObjective() || targetAI.ObjectiveManager.HasActiveObjective()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index 53e1b4ac6..d278433e8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -13,8 +14,7 @@ namespace Barotrauma Movement, Power, Maintenance, - Operate, - Undefined + Operate } class Order @@ -31,11 +31,7 @@ namespace Barotrauma return order; } - public Order Prefab - { - get; - private set; - } + public Order Prefab { get; private set; } public readonly string Name; @@ -55,7 +51,7 @@ namespace Barotrauma { return color.Value; } - else if (OrderCategoryIcons.TryGetValue(Category, out Tuple sprite)) + else if (Category.HasValue && OrderCategoryIcons.TryGetValue((OrderCategory)Category, out Tuple sprite)) { return sprite.Item2; } @@ -83,18 +79,28 @@ namespace Barotrauma public Character OrderGiver; - public readonly OrderCategory Category; + private readonly OrderCategory? category; + public OrderCategory? Category => category; //legacy support public readonly string[] AppropriateJobs; public readonly string[] Options; - public readonly string[] OptionNames; + private readonly Dictionary OptionNames; public readonly Dictionary OptionSprites; - public readonly float Weight; + private readonly Dictionary minimapIcons; + public Dictionary MinimapIcons => IsPrefab ? minimapIcons : Prefab.minimapIcons; - static Order() + public readonly float Weight; + public readonly bool MustSetTarget; + public readonly string AppropriateSkill; + + public bool HasOptions => (IsPrefab ? Options : Prefab.Options).Length > 1; + public bool IsPrefab { get; private set; } + public readonly bool MustManuallyAssign; + + public static void Init() { Prefabs = new Dictionary(); OrderCategoryIcons = new Dictionary>(); @@ -197,28 +203,24 @@ namespace Barotrauma TargetAllCharacters = orderElement.GetAttributeBool("targetallcharacters", false); AppropriateJobs = orderElement.GetAttributeStringArray("appropriatejobs", new string[0]); Options = orderElement.GetAttributeStringArray("options", new string[0]); - Category = (OrderCategory)Enum.Parse(typeof(OrderCategory), orderElement.GetAttributeString("category", "undefined"), true); + var category = orderElement.GetAttributeString("category", null); + if (!string.IsNullOrWhiteSpace(category)) { this.category = (OrderCategory)Enum.Parse(typeof(OrderCategory), category, true); } Weight = orderElement.GetAttributeFloat(0.0f, "weight"); + MustSetTarget = orderElement.GetAttributeBool("mustsettarget", false); + AppropriateSkill = orderElement.GetAttributeString("appropriateskill", null); - string translatedOptionNames = TextManager.Get("OrderOptions." + Identifier, true); - if (translatedOptionNames == null) + var optionNames = TextManager.Get("OrderOptions." + Identifier, true)?.Split(',', ',') ?? + orderElement.GetAttributeStringArray("optionnames", new string[0]); + OptionNames = new Dictionary(); + for (int i = 0; i < Options.Length && i < optionNames.Length; i++) { - OptionNames = orderElement.GetAttributeStringArray("optionnames", new string[0]); + OptionNames.Add(Options[i], optionNames[i].Trim()); } - else - { - string[] splitOptionNames = translatedOptionNames.Split(',', ','); - OptionNames = new string[Options.Length]; - for (int i = 0; i < Options.Length && i < splitOptionNames.Length; i++) - { - OptionNames[i] = splitOptionNames[i].Trim(); - } - } - - if (OptionNames.Length != Options.Length) + if (OptionNames.Count != Options.Length) { DebugConsole.ThrowError("Error in Order " + Name + " - the number of option names doesn't match the number of options."); - OptionNames = Options; + OptionNames.Clear(); + Options.ForEach(o => OptionNames.Add(o, o)); } var spriteElement = orderElement.GetChildElement("sprite"); @@ -241,18 +243,31 @@ namespace Barotrauma } } } + + minimapIcons = new Dictionary(); + var minimapIconElements = orderElement.GetChildElements("minimapicon"); + foreach (XElement minimapIconElement in minimapIconElements) + { + var id = minimapIconElement.GetAttributeString("id", null); + if (string.IsNullOrWhiteSpace(id)) { continue; } + minimapIcons.Add(id, new Sprite(minimapIconElement.GetChildElement("sprite"), lazyLoad: true)); + } + + IsPrefab = true; + MustManuallyAssign = orderElement.GetAttributeBool("mustmanuallyassign", false); } /// /// Constructor for order instances /// - public Order(Order prefab, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null) + public Order(Order prefab, Entity targetEntity, ItemComponent targetItem, Character orderGiver = null, bool isAutonomous = false) { Prefab = prefab; Name = prefab.Name; Identifier = prefab.Identifier; ItemComponentType = prefab.ItemComponentType; + ItemIdentifiers = prefab.ItemIdentifiers; Options = prefab.Options; SymbolSprite = prefab.SymbolSprite; Color = prefab.Color; @@ -261,22 +276,31 @@ namespace Barotrauma AppropriateJobs = prefab.AppropriateJobs; FadeOutTime = prefab.FadeOutTime; Weight = prefab.Weight; - Category = prefab.Category; - OrderGiver = orderGiver; + MustSetTarget = prefab.MustSetTarget; + AppropriateSkill = prefab.AppropriateSkill; + category = prefab.Category; + MustManuallyAssign = prefab.MustManuallyAssign; + OrderGiver = orderGiver; TargetEntity = targetEntity; if (targetItem != null) { if (UseController) { - //try finding the controller with the simpler non-recursive method first - ConnectedController = - targetItem.Item.GetConnectedComponents().FirstOrDefault() ?? - targetItem.Item.GetConnectedComponents(recursive: true).FirstOrDefault(); + ConnectedController = targetItem.Item?.FindController(); + if (ConnectedController == null) + { +#if DEBUG + throw new Exception("Tried to use controller, but couldn't find one"); +#endif + UseController = false; + } } TargetEntity = targetItem.Item; TargetItemComponent = targetItem; } + + IsPrefab = false; } public bool HasAppropriateJob(Character character) @@ -310,5 +334,51 @@ namespace Barotrauma return msg; } + + public List GetMatchingItems(Submarine submarine, bool mustBelongToPlayerSub) + { + List matchingItems = new List(); + if (submarine == null) { return matchingItems; } + if (ItemComponentType != null || ItemIdentifiers.Length > 0) + { + matchingItems = ItemIdentifiers.Length > 0 ? + Item.ItemList.FindAll(it => ItemIdentifiers.Contains(it.Prefab.Identifier) || it.HasTag(ItemIdentifiers)) : + Item.ItemList.FindAll(it => it.Components.Any(ic => ic.GetType() == ItemComponentType)); + if (mustBelongToPlayerSub) + { + matchingItems.RemoveAll(it => it.Submarine?.Info != null && it.Submarine.Info.Type != SubmarineInfo.SubmarineType.Player); + matchingItems.RemoveAll(it => it.Submarine != submarine && !submarine.DockedTo.Contains(it.Submarine)); + } + else + { + matchingItems.RemoveAll(it => it.Submarine != submarine); + } + matchingItems.RemoveAll(it => it.NonInteractable); + if (UseController) + { + matchingItems.RemoveAll(i => i.Components.None(c => c.GetType() == ItemComponentType) && !i.TryFindController(out _)); + } + } + return matchingItems; + } + + public List GetMatchingItems(bool mustBelongToPlayerSub) + { + Submarine submarine = Character.Controlled != null && Character.Controlled.TeamID == Character.TeamType.Team2 && Submarine.MainSubs.Length > 1 ? + Submarine.MainSubs[1] : + Submarine.MainSub; + return GetMatchingItems(submarine, mustBelongToPlayerSub); + } + + public string GetOptionName(string id) + { + return Prefab == null ? OptionNames[id] : Prefab.OptionNames[id]; + } + + public string GetOptionName(int index) + { + if (index < 0 || index >= Options.Length) { return null; } + return GetOptionName(Options[index]); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs index 405a12b9c..8ae8ce48a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PathFinder.cs @@ -46,7 +46,7 @@ namespace Barotrauma var nodes = new Dictionary(); foreach (WayPoint wayPoint in wayPoints) { - if (wayPoint == null) continue; + if (wayPoint == null) { continue; } if (nodes.ContainsKey(wayPoint.ID)) { #if DEBUG @@ -63,7 +63,7 @@ namespace Barotrauma { PathNode connectedNode = null; nodes.TryGetValue(linked.ID, out connectedNode); - if (connectedNode == null) continue; + if (connectedNode == null) { continue; } node.Value.connections.Add(connectedNode); } @@ -107,17 +107,17 @@ namespace Barotrauma void WaypointLinksChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { - if (Submarine.Unloading) return; + if (Submarine.Unloading) { return; } var waypoints = sender as IEnumerable; foreach (MapEntity me in waypoints) { WayPoint wp = me as WayPoint; - if (me == null) continue; + if (me == null) { continue; } var node = nodes.Find(n => n.Waypoint == wp); - if (node == null) return; + if (node == null) { return; } if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove) { @@ -136,10 +136,10 @@ namespace Barotrauma for (int i = 0; i < wp.linkedTo.Count; i++) { WayPoint connected = wp.linkedTo[i] as WayPoint; - if (connected == null) continue; + if (connected == null) { continue; } //already connected, continue - if (node.connections.Any(n => n.Waypoint == connected)) continue; + if (node.connections.Any(n => n.Waypoint == connected)) { continue; } var matchingNode = nodes.Find(n => n.Waypoint == connected); if (matchingNode == null) @@ -201,8 +201,8 @@ namespace Barotrauma if (body != null) { //if (body.UserData is Submarine) continue; - if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) continue; - if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) continue; + if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) { continue; } + if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { continue; } } } @@ -236,9 +236,9 @@ namespace Barotrauma if (InsideSubmarine) { //much higher cost to waypoints that are outside - if (node.Waypoint.CurrentHull == null) dist *= 10.0f; + if (node.Waypoint.CurrentHull == null) { dist *= 10.0f; } //avoid stopping at a doorway - if (node.Waypoint.ConnectedDoor != null) dist *= 10.0f; + if (node.Waypoint.ConnectedDoor != null) { dist *= 10.0f; } } if (dist < closestDist || endNode == null) { @@ -251,8 +251,8 @@ namespace Barotrauma if (body != null) { //if (body.UserData is Submarine) continue; - if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) continue; - if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) continue; + if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) { continue; } + if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { continue; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringPath.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringPath.cs index 86fbdfc8d..79c8dbefa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringPath.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/SteeringPath.cs @@ -9,12 +9,31 @@ namespace Barotrauma int currentIndex; + private float? totalLength; + public bool Unreachable { get; set; } + public float TotalLength + { + get + { + if (Unreachable) { return float.PositiveInfinity; } + if (!totalLength.HasValue) + { + totalLength = 0.0f; + for (int i = 0; i < nodes.Count - 1; i++) + { + totalLength += Vector2.Distance(nodes[i].WorldPosition, nodes[i + 1].WorldPosition); + } + } + return totalLength.Value; + } + } + public SteeringPath(bool unreachable = false) { nodes = new List(); @@ -23,10 +42,10 @@ namespace Barotrauma public void AddNode(WayPoint node) { - if (node == null) return; + if (node == null) { return; } nodes.Add(node); - if (node.CurrentHull == null) HasOutdoorsNodes = true; + if (node.CurrentHull == null) { HasOutdoorsNodes = true; } } public bool HasOutdoorsNodes @@ -48,10 +67,10 @@ namespace Barotrauma public WayPoint PrevNode { - get + get { - if (currentIndex-1 < 0 || currentIndex-1 > nodes.Count - 1) return null; - return nodes[currentIndex-1]; + if (currentIndex - 1 < 0 || currentIndex - 1 > nodes.Count - 1) { return null; } + return nodes[currentIndex - 1]; } } @@ -59,7 +78,7 @@ namespace Barotrauma { get { - if (currentIndex < 0 || currentIndex > nodes.Count - 1) return null; + if (currentIndex < 0 || currentIndex > nodes.Count - 1) { return null; } return nodes[currentIndex]; } } @@ -73,7 +92,7 @@ namespace Barotrauma { get { - if (currentIndex+1 < 0 || currentIndex+1 > nodes.Count - 1) return null; + if (currentIndex + 1 < 0 || currentIndex + 1 > nodes.Count - 1) { return null; } return nodes[currentIndex+1]; } } @@ -90,8 +109,8 @@ namespace Barotrauma public WayPoint CheckProgress(Vector2 simPosition, float minSimDistance = 0.1f) { - if (nodes.Count == 0 || currentIndex>nodes.Count-1) return null; - if (Vector2.Distance(simPosition, nodes[currentIndex].SimPosition) < minSimDistance) currentIndex++; + if (nodes.Count == 0 || currentIndex > nodes.Count - 1) { return null; } + if (Vector2.Distance(simPosition, nodes[currentIndex].SimPosition) < minSimDistance) { currentIndex++; } return CurrentNode; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs index 3bd42daaf..c2c82c00d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs @@ -8,7 +8,7 @@ using System; namespace Barotrauma { - class WreckAI : IServerSerializable + partial class WreckAI : IServerSerializable { public Submarine Wreck { get; private set; } @@ -16,6 +16,7 @@ namespace Barotrauma private readonly List allItems; private readonly List thalamusItems; + private readonly List thalamusStructures; private readonly List turrets = new List(); private readonly List wayPoints = new List(); private readonly List hulls = new List(); @@ -28,17 +29,89 @@ namespace Barotrauma private bool IsClient => GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; - public WreckAI(Submarine wreck, Item brain, List items = null) + private bool IsThalamus(MapEntityPrefab entityPrefab) => IsThalamus(entityPrefab, Config.Entity); + + private static IEnumerable GetThalamusEntities(Submarine wreck, string tag) where T : MapEntity => GetThalamusEntities(wreck, tag).Where(e => e is T).Select(e => e as T); + + private static IEnumerable GetThalamusEntities(Submarine wreck, string tag) => MapEntity.mapEntityList.Where(e => e.Submarine == wreck && e.prefab != null && IsThalamus(e.prefab, tag)); + + private static bool IsThalamus(MapEntityPrefab entityPrefab, string tag) => entityPrefab.Category == MapEntityCategory.Thalamus || entityPrefab.Tags.Contains(tag); + + public WreckAI(Submarine wreck) { + Wreck = wreck; Config = WreckAIConfig.GetRandom(); if (Config == null) { DebugConsole.ThrowError("WreckAI: No wreck AI config found!"); - Kill(); return; } - allItems = items ?? wreck.GetItems(false); - thalamusItems = allItems.FindAll(i => i.Prefab.Category == MapEntityCategory.Thalamus || i.HasTag("thalamus")); + var thalamusPrefabs = ItemPrefab.Prefabs.Where(p => IsThalamus(p)); + var brainPrefab = thalamusPrefabs.GetRandom(i => i.Tags.Contains(Config.Brain), Rand.RandSync.Server); + if (brainPrefab == null) + { + DebugConsole.ThrowError($"WreckAI: Could not find any brain prefab with the tag {Config.Brain}! Cannot continue. Failed to create wreck AI."); + return; + } + allItems = Wreck.GetItems(false); + thalamusItems = allItems.FindAll(i => IsThalamus(i.prefab)); + var hulls = Wreck.GetHulls(false); + brain = new Item(brainPrefab, Vector2.Zero, Wreck); + thalamusItems.Add(brain); + Vector2 negativeMargin = new Vector2(40, 20); + Vector2 minSize = brain.Rect.Size.ToVector2() - negativeMargin; + Vector2 maxSize = new Vector2(brain.Rect.Width * 3, brain.Rect.Height * 3); + // First try to get a room that is not too big and not in the edges of the sub. + // Also try not to create the brain in a room that already have carrier items inside. + // Ignore hulls that have any linked hulls to keep the calculations simple. + // Shrink the horizontal axis so that the brain is not placed in the left or right side, where we often have curved walls. + // Also ignore hulls that have open gaps, because we'll want the room to be full of water. The room will be filled with water when the brain is inserted in the room. + Rectangle shrinkedBounds = ToolBox.GetWorldBounds(Wreck.WorldPosition.ToPoint(), new Point(Wreck.Borders.Width - 500, Wreck.Borders.Height)); + bool BaseCondition(Hull h) => h.RectWidth > minSize.X && h.RectHeight > minSize.Y && h.GetLinkedEntities().None() && h.ConnectedGaps.None(g => g.Open > 0); + bool IsNotTooBig(Hull h) => h.RectWidth < maxSize.X && h.RectHeight < maxSize.Y; + bool IsNotInFringes(Hull h) => shrinkedBounds.ContainsWorld(h.WorldRect); + bool DoesNotContainOtherItems(Hull h) => thalamusItems.None(i => i.CurrentHull == h); + Hull brainHull = hulls.GetRandom(h => BaseCondition(h) && IsNotTooBig(h) && IsNotInFringes(h) && DoesNotContainOtherItems(h), Rand.RandSync.Server); + if (brainHull == null) + { + brainHull = hulls.GetRandom(h => BaseCondition(h) && IsNotInFringes(h) && DoesNotContainOtherItems(h), Rand.RandSync.Server); + } + if (brainHull == null) + { + brainHull = hulls.GetRandom(h => BaseCondition(h) && (IsNotInFringes(h) || DoesNotContainOtherItems(h)), Rand.RandSync.Server); + } + if (brainHull == null) + { + brainHull = hulls.GetRandom(BaseCondition, Rand.RandSync.Server); + } + var thalamusStructurePrefabs = StructurePrefab.Prefabs.Where(p => IsThalamus(p)); + if (brainHull == null) { return; } + brainHull.WaterVolume = brainHull.Volume; + brain.SetTransform(brainHull.SimPosition, rotation: 0, findNewHull: false); + brain.CurrentHull = brainHull; + var backgroundPrefab = thalamusStructurePrefabs.GetRandom(i => i.Tags.Contains(Config.BrainRoomBackground), Rand.RandSync.Server); + if (backgroundPrefab != null) + { + new Structure(brainHull.Rect, backgroundPrefab, Wreck); + } + var horizontalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomHorizontalWall), Rand.RandSync.Server); + if (horizontalWallPrefab != null) + { + int height = (int)horizontalWallPrefab.Size.Y; + int halfHeight = height / 2; + int quarterHeight = halfHeight / 2; + new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, Wreck); + new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top - brainHull.Rect.Height + halfHeight + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, Wreck); + } + var verticalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomVerticalWall), Rand.RandSync.Server); + if (verticalWallPrefab != null) + { + int width = (int)verticalWallPrefab.Size.X; + int halfWidth = width / 2; + int quarterWidth = halfWidth / 2; + new Structure(new Rectangle(brainHull.Rect.Left - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, Wreck); + new Structure(new Rectangle(brainHull.Rect.Right - halfWidth - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, Wreck); + } foreach (Item item in allItems) { if (thalamusItems.Contains(item)) @@ -62,7 +135,7 @@ namespace Barotrauma if (MapEntityPrefab.List.GetRandom(e => e is ItemPrefab i && container.CanBeContained(i) && Config.ForbiddenAmmunition.None(id => id.Equals(i.Identifier, StringComparison.OrdinalIgnoreCase)), Rand.RandSync.Server) is ItemPrefab ammoPrefab) { - Item ammo = new Item(ammoPrefab, container.Item.WorldPosition, wreck); + Item ammo = new Item(ammoPrefab, container.Item.WorldPosition, Wreck); if (!container.Inventory.TryPutItem(ammo, i, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: false)) { item.Remove(); @@ -73,16 +146,14 @@ namespace Barotrauma } } } - this.brain = brain; - Wreck = wreck; - foreach (var item in Wreck.GetItems(false)) + foreach (var item in allItems) { var turret = item.GetComponent(); if (turret != null) { turrets.Add(turret); } - if (item.HasTag("cellspawnorgan")) + if (item.HasTag(Config.Spawner)) { if (!spawnOrgans.Contains(item)) { @@ -93,19 +164,22 @@ namespace Barotrauma wayPoints.AddRange(Wreck.GetWaypoints(false)); hulls.AddRange(Wreck.GetHulls(false)); IsAlive = true; + thalamusStructures = GetThalamusEntities(Wreck, Config.Entity).ToList(); } private readonly List destroyedOrgans = new List(); public void Update(float deltaTime) { - if (!IsAlive || Wreck == null || Wreck.Removed) + if (!IsAlive) { return; } + if (Wreck == null || Wreck.Removed) { - cells.ForEach(c => c.OnDeath -= OnCellDeath); + Remove(); return; } if (brain == null || brain.Removed || brain.Condition <= 0) { Kill(); + return; } destroyedOrgans.Clear(); foreach (var organ in spawnOrgans) @@ -116,7 +190,6 @@ namespace Barotrauma } } destroyedOrgans.ForEach(o => spawnOrgans.Remove(o)); - bool someoneNearby = false; float minDist = Sonar.DefaultSonarRange * 2.0f; foreach (Submarine submarine in Submarine.Loaded) @@ -138,7 +211,6 @@ namespace Barotrauma } } if (!someoneNearby) { return; } - OperateTurrets(deltaTime); if (!IsClient) { @@ -164,7 +236,7 @@ namespace Barotrauma } int cellsOutside = Rand.Range(MinCellsOutside, MaxCellsOutside); // If we failed to spawn some of the cells in the brainroom/inside, spawn some extra cells outside. - cellsOutside = Math.Clamp(cellsOutside + brainRoomCells + cellsInside - cells.Count, cellsOutside, MaxCellsOutside); + cellsOutside = Math.Clamp(cellsOutside + brainRoomCells + cellsInside - protectiveCells.Count, cellsOutside, MaxCellsOutside); for (int i = 0; i < cellsOutside; i++) { ISpatialEntity targetEntity = wayPoints.GetRandom(wp => wp.CurrentHull == null); @@ -174,35 +246,93 @@ namespace Barotrauma initialCellsSpawned = true; } - public void Kill() + private void Kill() { + thalamusItems.ForEach(i => i.Condition = 0); + foreach (var turret in turrets) + { + // Snap all tendons + foreach (Item item in turret.ActiveProjectiles) + { + if (item.GetComponent()?.IsStuckToTarget ?? false) + { + item.Condition = 0; + } + } + } + FadeOutColors(); + protectiveCells.ForEach(c => c.OnDeath -= OnCellDeath); if (!IsClient) { - brain.Condition = 0; + if (Config != null) + { + if (Config.KillAgentsWhenEntityDies) + { + protectiveCells.ForEach(c => c.Kill(CauseOfDeathType.Unknown, null)); + if (!string.IsNullOrWhiteSpace(Config.OffensiveAgent)) + { + foreach (var character in Character.CharacterList) + { + // Kills ALL offensive agents that are near the thalamus. Not the ideal solution, + // but as long as spawning is handled via status effects, I don't know if there is any better way. + // In practice there shouldn't be terminal cells from different thalamus organisms at the same time. + // And if there was, the distance check should prevent killing the agents of a different organism. + if (character.SpeciesName.Equals(Config.OffensiveAgent, StringComparison.OrdinalIgnoreCase)) + { + // Sonar distance is used also for wreck positioning. No wreck should be closer to each other than this. + float maxDistance = Sonar.DefaultSonarRange; + if (Vector2.DistanceSquared(character.WorldPosition, Wreck.WorldPosition) < maxDistance * maxDistance) + { + character.Kill(CauseOfDeathType.Unknown, null); + } + } + } + } + } + } } + protectiveCells.Clear(); IsAlive = false; } + partial void FadeOutColors(); + + public void Remove() + { + Kill(); + RemoveThalamusItems(Wreck); + thalamusItems?.Clear(); + thalamusStructures?.Clear(); + } + + public static void RemoveThalamusItems(Submarine wreck) + { + foreach (var wreckAiConfig in WreckAIConfig.List) + { + GetThalamusEntities(wreck, wreckAiConfig.Entity).ForEachMod(e => e.Remove()); + } + } + // The client doesn't use these, so we don't have to sync them. - private readonly List cells = new List(); + private readonly List protectiveCells = new List(); // Intentionally contains duplicates. private readonly List populatedHulls = new List(); private float cellSpawnTimer; - private float CellSpawnTime => Config.CellSpawnTime; - private float CellSpawnRandomFactor => Config.CellSpawnRandomFactor; - private int MinCellsPerBrainRoom => Config.MinCellsPerBrainRoom; - private int MaxCellsPerRoom => Config.MaxCellsPerRoom; - private int MinCellsOutside => Config.MinCellsOutside; - private int MaxCellsOutside => Config.MaxCellsOutside; - private int MinCellsInside => Config.MinCellsInside; - private int MaxCellsInside => Config.MaxCellsInside; - private int MaxCellCount => Config.MaxCellCount; + private float CellSpawnTime => Config.AgentSpawnDelay; + private float CellSpawnRandomFactor => Config.AgentSpawnDelayRandomFactor; + private int MinCellsPerBrainRoom => Config.MinAgentsPerBrainRoom; + private int MaxCellsPerRoom => Config.MaxAgentsPerRoom; + private int MinCellsOutside => Config.MinAgentsOutside; + private int MaxCellsOutside => Config.MaxAgentsOutside; + private int MinCellsInside => Config.MinAgentsInside; + private int MaxCellsInside => Config.MaxAgentsInside; + private int MaxCellCount => Config.MaxAgentCount; private float MinWaterLevel => Config.MinWaterLevel; void UpdateReinforcements(float deltaTime) { - if (cells.Count >= MaxCellCount) { return; } + if (protectiveCells.Count >= MaxCellCount || spawnOrgans.Count == 0) { return; } cellSpawnTimer -= deltaTime; if (cellSpawnTimer < 0) { @@ -214,7 +344,7 @@ namespace Barotrauma bool TrySpawnCell(out Character cell, ISpatialEntity targetEntity = null) { cell = null; - if (cells.Count >= MaxCellCount) { return false; } + if (protectiveCells.Count >= MaxCellCount) { return false; } if (targetEntity == null) { targetEntity = @@ -231,8 +361,8 @@ namespace Barotrauma populatedHulls.Add(wp.CurrentHull); } // Don't add items in the list, because we want to be able to ignore the restrictions for spawner organs. - cell = Character.Create("Leucocyte", targetEntity.WorldPosition, ToolBox.RandomSeed(8), hasAi: true, createNetworkEvent: true); - cells.Add(cell); + cell = Character.Create(Config.DefensiveAgent, targetEntity.WorldPosition, ToolBox.RandomSeed(8), hasAi: true, createNetworkEvent: true); + protectiveCells.Add(cell); cell.OnDeath += OnCellDeath; cellSpawnTimer = CellSpawnTime * Rand.Range(CellSpawnRandomFactor, 1 + CellSpawnRandomFactor); return true; @@ -243,7 +373,7 @@ namespace Barotrauma foreach (var turret in turrets) { // Never target other creatures than humans with the turrets. - turret.ThalamusOperate(deltaTime, + turret.ThalamusOperate(this, deltaTime, !turret.Item.HasTag("ignorecharacters"), targetOtherCreatures: false, !turret.Item.HasTag("ignoresubmarines"), @@ -253,7 +383,7 @@ namespace Barotrauma void OnCellDeath(Character character, CauseOfDeath causeOfDeath) { - cells.Remove(character); + protectiveCells.Remove(character); } #if SERVER @@ -261,12 +391,6 @@ namespace Barotrauma { msg.Write(IsAlive); } -#endif -#if CLIENT - public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) - { - IsAlive = msg.ReadBoolean(); - } #endif } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs index 76a6f2758..9830a8d4d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs @@ -12,35 +12,68 @@ namespace Barotrauma public Dictionary SerializableProperties { get; private set; } + [Serialize("", false)] + public string Entity { get; private set; } + + [Serialize("", false)] + public string DefensiveAgent { get; private set; } + + [Serialize("", false)] + public string OffensiveAgent { get; private set; } + + [Serialize("", false)] + public string Brain { get; private set; } + + [Serialize("", false)] + public string Spawner { get; private set; } + + [Serialize("", false)] + public string BrainRoomBackground { get; private set; } + + [Serialize("", false)] + public string BrainRoomVerticalWall { get; private set; } + + [Serialize("", false)] + public string BrainRoomHorizontalWall { get; private set; } + [Serialize(60f, false)] - public float CellSpawnTime { get; set; } + public float AgentSpawnDelay { get; private set; } [Serialize(0.5f, false)] - public float CellSpawnRandomFactor { get; set; } + public float AgentSpawnDelayRandomFactor { get; private set; } [Serialize(0, false)] - public int MinCellsPerBrainRoom { get; set; } + public int MinAgentsPerBrainRoom { get; private set; } [Serialize(3, false)] - public int MaxCellsPerRoom { get; set; } + public int MaxAgentsPerRoom { get; private set; } [Serialize(2, false)] - public int MinCellsOutside { get; set; } + public int MinAgentsOutside { get; private set; } [Serialize(5, false)] - public int MaxCellsOutside { get; set; } + public int MaxAgentsOutside { get; private set; } [Serialize(3, false)] - public int MinCellsInside { get; set; } + public int MinAgentsInside { get; private set; } [Serialize(10, false)] - public int MaxCellsInside { get; set; } + public int MaxAgentsInside { get; private set; } [Serialize(15, false)] - public int MaxCellCount { get; set; } + public int MaxAgentCount { get; private set; } [Serialize(100f, false)] - public float MinWaterLevel { get; set; } + public float MinWaterLevel { get; private set; } + + [Serialize(true, false)] + public bool KillAgentsWhenEntityDies { get; private set; } + + [Serialize(1f, false)] + public float DeadEntityColorMultiplier { get; private set; } + + [Serialize(1f, false)] + public float DeadEntityColorFadeOutTime { get; private set; } public readonly string[] ForbiddenAmmunition; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs index 8ffcb47bb..f6001bd17 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs @@ -46,9 +46,13 @@ namespace Barotrauma { base.Update(deltaTime, cam); - if (!Enabled) return; + if (!Enabled) { return; } + if (IsDead || Vitality <= 0.0f || Stun > 0.0f || IsIncapacitated) { return; } - if (!IsRemotePlayer) + //don't enable simple physics on dead/incapacitated characters + //the ragdoll controls the movement of incapacitated characters instead of the collider, + //but in simple physics mode the ragdoll would get disabled, causing the character to not move at all + if (!IsRemotePlayer && !(AIController is HumanAIController)) { float characterDist = float.MaxValue; #if CLIENT @@ -70,10 +74,9 @@ namespace Barotrauma } } - if (IsDead || Vitality <= 0.0f|| Stun > 0.0f || IsIncapacitated) return; - if (!aiController.Enabled) return; - if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) return; - if (Controlled == this) return; + if (!aiController.Enabled) { return; } + if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; } + if (Controlled == this) { return; } if (!IsRemotePlayer) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index 79fea4873..859b76516 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -76,7 +76,8 @@ namespace Barotrauma { if (InWater || !CanWalk) { - return TargetMovement.Length() > (SwimSlowParams.MovementSpeed + SwimFastParams.MovementSpeed) / 2.0f; + float avg = (SwimSlowParams.MovementSpeed + SwimFastParams.MovementSpeed) / 2.0f; + return TargetMovement.LengthSquared() > avg * avg; } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index bcb29ad85..12f9c5bf5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -213,7 +213,12 @@ namespace Barotrauma UpdateWalkAnim(deltaTime); } - //don't flip or drag when simply physics is enabled + if (character.SelectedCharacter != null) + { + DragCharacter(character.SelectedCharacter, deltaTime); + } + + //don't flip when simply physics is enabled if (SimplePhysicsEnabled) { return; } if (!character.IsRemotePlayer && (character.AIController == null || character.AIController.CanFlip)) @@ -248,29 +253,33 @@ namespace Barotrauma } } - if (character.SelectedCharacter != null) - { - DragCharacter(character.SelectedCharacter, deltaTime); - } - if (!CurrentFishAnimation.Flip) { return; } if (IsStuck) { return; } if (character.AIController != null && !character.AIController.CanFlip) { return; } flipCooldown -= deltaTime; - - if (TargetDir != Direction.None && TargetDir != dir) + if (TargetDir != Direction.None && TargetDir != dir) { flipTimer += deltaTime; - if ((flipTimer > 0.5f && flipCooldown <= 0.0f) || character.IsRemotePlayer) + // Speed reductions are not taken into account here. It's intentional: an ai character cannot flip if it's heavily paralyzed (for example). + float requiredSpeed = CurrentAnimationParams.MovementSpeed / 2; + if (CurrentHull != null) + { + // Enemy movement speeds are halved inside submarines + requiredSpeed /= 2; + } + bool isMovingFastEnough = Math.Abs(MainLimb.LinearVelocity.X) > requiredSpeed; + bool isTryingToMoveHorizontally = Math.Abs(TargetMovement.X) > Math.Abs(TargetMovement.Y); + if ((flipTimer > CurrentFishAnimation.FlipDelay && flipCooldown <= 0.0f && ((isMovingFastEnough && isTryingToMoveHorizontally) || IsMovingBackwards)) + || character.IsRemotePlayer) { Flip(); if (!inWater || (CurrentSwimParams != null && CurrentSwimParams.Mirror)) { - Mirror(); + Mirror(CurrentSwimParams != null ? CurrentSwimParams.MirrorLerp : true); } flipTimer = 0.0f; - flipCooldown = 1.0f; + flipCooldown = CurrentFishAnimation.FlipCooldown; } } else @@ -295,7 +304,7 @@ namespace Barotrauma if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) { //stop dragging if there's something between the pull limb and the target - Vector2 sourceSimPos = mouthLimb.SimPosition; + Vector2 sourceSimPos = SimplePhysicsEnabled ? character.SimPosition : mouthLimb.SimPosition; Vector2 targetSimPos = target.SimPosition; if (character.Submarine != null && character.SelectedCharacter.Submarine == null) { @@ -317,7 +326,7 @@ namespace Barotrauma float eatSpeed = dmg / ((float)Math.Sqrt(Math.Max(target.Mass, 1)) * 10); eatTimer += deltaTime * eatSpeed; - Vector2 mouthPos = GetMouthPosition().Value; + Vector2 mouthPos = SimplePhysicsEnabled ? character.SimPosition : GetMouthPosition().Value; Vector2 attackSimPosition = character.Submarine == null ? ConvertUnits.ToSimUnits(target.WorldPosition) : target.SimPosition; Vector2 limbDiff = attackSimPosition - mouthPos; @@ -525,6 +534,7 @@ namespace Barotrauma foreach (var limb in Limbs) { + if (limb.IsSevered) { continue; } if (Math.Abs(limb.Params.ConstantTorque) > 0) { limb.body.SmoothRotate(movementAngle + MathHelper.ToRadians(limb.Params.ConstantAngle) * Dir, limb.Params.ConstantTorque, wrapAngle: true); @@ -550,10 +560,12 @@ namespace Barotrauma for (int i = 0; i < Limbs.Length; i++) { - if (Limbs[i].SteerForce <= 0.0f) { continue; } + var limb = Limbs[i]; + if (limb.IsSevered) { continue; } + if (limb.SteerForce <= 0.0f) { continue; } if (!Collider.PhysEnabled) { continue; } - Vector2 pullPos = Limbs[i].PullJointWorldAnchorA; - Limbs[i].body.ApplyForce(movement * Limbs[i].SteerForce * Limbs[i].Mass * Math.Max(character.SpeedMultiplier, 1), pullPos); + Vector2 pullPos = limb.PullJointWorldAnchorA; + limb.body.ApplyForce(movement * limb.SteerForce * limb.Mass * Math.Max(character.SpeedMultiplier, 1), pullPos); } Vector2 mainLimbDiff = mainLimb.PullJointWorldAnchorB - mainLimb.SimPosition; @@ -604,6 +616,14 @@ namespace Barotrauma float stepLift = TargetMovement.X == 0.0f ? 0 : (float)Math.Sin(WalkPos * CurrentGroundedParams.StepLiftFrequency + MathHelper.Pi * CurrentGroundedParams.StepLiftOffset) * (CurrentGroundedParams.StepLiftAmount / 100); + float limpAmount = character.GetLegPenalty(); + if (limpAmount > 0) + { + float walkPosX = (float)Math.Cos(WalkPos); + //make the footpos oscillate when limping + limpAmount = Math.Max(Math.Abs(walkPosX) * limpAmount, 0.0f) * Math.Min(Math.Abs(TargetMovement.X), 0.3f) * Dir; + } + Limb torso = GetLimb(LimbType.Torso); if (torso != null) { @@ -613,7 +633,7 @@ namespace Barotrauma } if (TorsoPosition.HasValue) { - Vector2 pos = colliderBottom + new Vector2(0, TorsoPosition.Value + stepLift); + Vector2 pos = colliderBottom + new Vector2(limpAmount, TorsoPosition.Value + stepLift); if (torso != mainLimb) { @@ -635,7 +655,7 @@ namespace Barotrauma } if (HeadPosition.HasValue) { - Vector2 pos = colliderBottom + new Vector2(0, HeadPosition.Value + stepLift * CurrentGroundedParams.StepLiftHeadMultiplier); + Vector2 pos = colliderBottom + new Vector2(limpAmount, HeadPosition.Value + stepLift * CurrentGroundedParams.StepLiftHeadMultiplier); if (head != mainLimb) { @@ -670,6 +690,7 @@ namespace Barotrauma foreach (Limb limb in Limbs) { + if (limb.IsSevered) { continue; } if (Math.Abs(limb.Params.ConstantTorque) > 0) { limb.body.SmoothRotate(movementAngle + MathHelper.ToRadians(limb.Params.ConstantAngle) * Dir, limb.Params.ConstantTorque, wrapAngle: true); @@ -766,6 +787,7 @@ namespace Barotrauma foreach (Limb limb in Limbs) { + if (limb.IsSevered) { continue; } #if CLIENT if (limb.LightSource != null) { @@ -821,6 +843,7 @@ namespace Barotrauma base.Flip(); foreach (Limb l in Limbs) { + if (l.IsSevered) { continue; } if (!l.DoesFlip) { continue; } if (RagdollParams.IsSpritesheetOrientationHorizontal) { @@ -838,10 +861,13 @@ namespace Barotrauma foreach (Limb l in Limbs) { + if (l.IsSevered) { continue; } + TrySetLimbPosition(l, centerOfMass, new Vector2(centerOfMass.X - (l.SimPosition.X - centerOfMass.X), l.SimPosition.Y), lerp); + l.body.PositionSmoothingFactor = 0.8f; if (!l.DoesFlip) { continue; } @@ -862,7 +888,7 @@ namespace Barotrauma if (diff < 100.0f) { character.SelectedCharacter.AnimController.SetPosition( - new Vector2(centerOfMass.X - diff, character.SelectedCharacter.SimPosition.Y), lerp: true); + new Vector2(centerOfMass.X - diff, character.SelectedCharacter.SimPosition.Y), lerp); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 059d0fb65..f20290277 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -323,7 +323,14 @@ namespace Barotrauma levitatingCollider = true; ColliderIndex = Crouching ? 1 : 0; - if (!Crouching && ColliderIndex == 1) Crouching = true; + if (character.SelectedConstruction?.GetComponent()?.ControlCharacterPose ?? false) + { + Crouching = false; + } + else if (!Crouching && ColliderIndex == 1) + { + Crouching = true; + } //stun (= disable the animations) if the ragdoll receives a large enough impact if (strongestImpact > 0.0f) @@ -441,7 +448,7 @@ namespace Barotrauma for (int i = -1; i < 2; i += 2) { Vector2 footPos = GetColliderBottom(); - footPos = new Vector2(waist.SimPosition.X + Math.Sign(StepSize.Value.X * i) * Dir * 0.3f, footPos.Y - 0.1f * RagdollParams.JointScale); + footPos = new Vector2(waist.SimPosition.X + Math.Sign(WalkParams.StepSize.X * i) * Dir * 0.3f, footPos.Y - 0.1f * RagdollParams.JointScale); var foot = i == -1 ? rightFoot : leftFoot; MoveLimb(foot, footPos, Math.Abs(foot.SimPosition.X - footPos.X) * 100.0f, true); } @@ -481,7 +488,7 @@ namespace Barotrauma swimmingStateLockTimer -= deltaTime; - if (forceStanding) + if (forceStanding || character.AnimController.AnimationTestPose) { swimming = false; } @@ -542,12 +549,6 @@ namespace Barotrauma Limb leftLeg = GetLimb(LimbType.LeftLeg); Limb rightLeg = GetLimb(LimbType.RightLeg); - float limpAmount = - character.CharacterHealth.GetAfflictionStrength("damage", leftFoot, true) + - character.CharacterHealth.GetAfflictionStrength("damage", rightFoot, true) + - character.CharacterHealth.GetAfflictionStrength("spaceherpes"); - limpAmount = MathHelper.Clamp(limpAmount / 100.0f, 0.0f, 1.0f); - float walkCycleMultiplier = 1.0f; if (Stairs != null) { @@ -582,6 +583,11 @@ namespace Barotrauma stepSize.Y *= walkPosY; float footMid = colliderPos.X; + + var herpes = character.CharacterHealth.GetAffliction("spaceherpes", false); + float herpesAmount = herpes == null ? 0 : herpes.Strength / herpes.Prefab.MaxStrength; + float legDamage = character.GetLegPenalty(startSum: -0.1f) * 1.1f; + float limpAmount = MathHelper.Lerp(0, 1, legDamage + herpesAmount); if (limpAmount > 0.0f) { //make the footpos oscillate when limping @@ -652,6 +658,7 @@ namespace Barotrauma (float)Math.Sin(WalkPos * CurrentGroundedParams.StepLiftFrequency + MathHelper.Pi * CurrentGroundedParams.StepLiftOffset) * (CurrentGroundedParams.StepLiftAmount / 100); float y = colliderPos.Y + stepLift; + if (TorsoPosition.HasValue) { y += TorsoPosition.Value; @@ -690,6 +697,7 @@ namespace Barotrauma foreach (Limb limb in Limbs) { + if (limb.IsSevered) { continue; } MoveLimb(limb, limb.SimPosition + move, 15.0f, true); } @@ -947,8 +955,6 @@ namespace Barotrauma torso.body.MoveToPos(Collider.SimPosition + new Vector2((float)Math.Sin(-Collider.Rotation), (float)Math.Cos(-Collider.Rotation)) * 0.4f, 5.0f); - if (TargetMovement == Vector2.Zero) { return; } - movement = MathUtils.SmoothStep(movement, TargetMovement, 0.3f); if (TorsoAngle.HasValue) @@ -1001,29 +1007,31 @@ namespace Barotrauma } WalkPos += movement.Length(); - legCyclePos += Vector2.Normalize(movement).Length(); + legCyclePos += Math.Min(movement.LengthSquared() + Collider.AngularVelocity, 1.0f); handCyclePos += MathHelper.ToRadians(CurrentSwimParams.HandCycleSpeed) * Math.Sign(movement.X); var waist = GetLimb(LimbType.Waist); footPos = waist == null ? Vector2.Zero : waist.SimPosition - new Vector2((float)Math.Sin(-Collider.Rotation), (float)Math.Cos(-Collider.Rotation)) * (upperLegLength + lowerLegLength); - Vector2 transformedFootPos = new Vector2((float)Math.Sin(legCyclePos / CurrentSwimParams.LegCycleLength / character.SpeedMultiplier) * CurrentSwimParams.LegMoveAmount, 0.0f); + Vector2 transformedFootPos = new Vector2((float)Math.Sin(legCyclePos / CurrentSwimParams.LegCycleLength) * CurrentSwimParams.LegMoveAmount, 0.0f); transformedFootPos = Vector2.Transform(transformedFootPos, Matrix.CreateRotationZ(Collider.Rotation)); + float torque = CurrentSwimParams.FootRotateStrength * character.SpeedMultiplier * (1.2f - character.GetLegPenalty()); if (rightFoot != null && !rightFoot.Disabled) { - FootIK(rightFoot, footPos - transformedFootPos, CurrentSwimParams.FootRotateStrength, CurrentSwimParams.FootRotateStrength, CurrentSwimParams.FootAngleInRadians); + FootIK(rightFoot, footPos - transformedFootPos, torque, torque, CurrentSwimParams.FootAngleInRadians); } if (leftFoot != null && !leftFoot.Disabled) { - FootIK(leftFoot, footPos + transformedFootPos, CurrentSwimParams.FootRotateStrength, CurrentSwimParams.FootRotateStrength, CurrentSwimParams.FootAngleInRadians); + FootIK(leftFoot, footPos + transformedFootPos, torque, torque, CurrentSwimParams.FootAngleInRadians); } handPos = (torso.SimPosition + head.SimPosition) / 2.0f; - //at the surface, not moving sideways -> hands just float around - if (!headInWater && TargetMovement.X == 0.0f && TargetMovement.Y > 0) + //at the surface, not moving sideways OR not moving at all + // -> hands just float around + if ((!headInWater && TargetMovement.X == 0.0f && TargetMovement.Y > 0) || TargetMovement.LengthSquared() < 0.001f) { - handPos.X = handPos.X + Dir * 0.6f; + handPos += MathUtils.RotatePoint(Vector2.UnitX * Dir * 0.6f, torso.Rotation); float wobbleAmount = 0.1f; @@ -1060,7 +1068,7 @@ namespace Barotrauma rightHandPos.X = (Dir == 1.0f) ? Math.Max(0.3f, rightHandPos.X) : Math.Min(-0.3f, rightHandPos.X); rightHandPos = Vector2.Transform(rightHandPos, rotationMatrix); - HandIK(rightHand, handPos + rightHandPos, CurrentSwimParams.HandMoveStrength * character.SpeedMultiplier); + HandIK(rightHand, handPos + rightHandPos, CurrentSwimParams.HandMoveStrength * character.SpeedMultiplier * (1 - Character.GetRightHandPenalty())); } if (leftHand != null && !leftHand.Disabled) @@ -1069,7 +1077,7 @@ namespace Barotrauma leftHandPos.X = (Dir == 1.0f) ? Math.Max(0.3f, leftHandPos.X) : Math.Min(-0.3f, leftHandPos.X); leftHandPos = Vector2.Transform(leftHandPos, rotationMatrix); - HandIK(leftHand, handPos + leftHandPos, CurrentSwimParams.HandMoveStrength * character.SpeedMultiplier); + HandIK(leftHand, handPos + leftHandPos, CurrentSwimParams.HandMoveStrength * character.SpeedMultiplier * (1 - Character.GetLeftHandPenalty())); } } @@ -1251,31 +1259,39 @@ namespace Barotrauma Limb head = GetLimb(LimbType.Head); Limb torso = GetLimb(LimbType.Torso); - - //if the head is moving, try to protect it with the hands - if (head.LinearVelocity.LengthSquared() > 1.0f && !head.IsSevered) + + if (head != null && head.LinearVelocity.LengthSquared() > 1.0f && !head.IsSevered) { + //if the head is moving, try to protect it with the hands Limb leftHand = GetLimb(LimbType.LeftHand); Limb rightHand = GetLimb(LimbType.RightHand); //move hands in front of the head in the direction of the movement Vector2 protectPos = head.SimPosition + Vector2.Normalize(head.LinearVelocity); - if (!rightHand.IsSevered) HandIK(rightHand, protectPos, strength * 0.1f); - if (!leftHand.IsSevered) HandIK(leftHand, protectPos, strength * 0.1f); + if (rightHand != null && !rightHand.IsSevered) + { + HandIK(rightHand, protectPos, strength * 0.1f); + } + if (leftHand != null && !leftHand.IsSevered) + { + HandIK(leftHand, protectPos, strength * 0.1f); + } } + if (torso == null) { return; } //attempt to make legs stay in a straight line with the torso to prevent the character from doing a split for (int i = 0; i < 2; i++) { var thigh = i == 0 ? GetLimb(LimbType.LeftThigh) : GetLimb(LimbType.RightThigh); - if (thigh.IsSevered) continue; + if (thigh == null) { continue; } + if (thigh.IsSevered) { continue; } float thighDiff = Math.Abs(MathUtils.GetShortestAngle(torso.Rotation, thigh.Rotation)); float thighTorque = thighDiff * thigh.Mass * Math.Sign(torso.Rotation - thigh.Rotation) * 5.0f; thigh.body.ApplyTorque(thighTorque * strength); var leg = i == 0 ? GetLimb(LimbType.LeftLeg) : GetLimb(LimbType.RightLeg); - if (leg.IsSevered) continue; + if (leg == null || leg.IsSevered) { continue; } float legDiff = Math.Abs(MathUtils.GetShortestAngle(torso.Rotation, leg.Rotation)); float legTorque = legDiff * leg.Mass * Math.Sign(torso.Rotation - leg.Rotation) * 5.0f; leg.body.ApplyTorque(legTorque * strength); @@ -1410,7 +1426,7 @@ namespace Barotrauma SteamAchievementManager.OnCharacterRevived(target, character); lastReviveTime = (float)Timing.TotalTime; #if SERVER - GameMain.Server?.KarmaManager?.OnCharacterHealthChanged(target, character, damage: Math.Min(prevVitality - target.Vitality, 0.0f)); + GameMain.Server?.KarmaManager?.OnCharacterHealthChanged(target, character, damage: Math.Min(prevVitality - target.Vitality, 0.0f), stun: 0.0f); #endif //reset attacker, we don't want the character to start attacking us //because we caused a bit of damage to them during CPR @@ -1540,6 +1556,11 @@ namespace Barotrauma { sourceSimPos -= character.SelectedCharacter.Submarine.SimPosition; } + else if (character.Submarine != null && character.SelectedCharacter.Submarine != null && character.Submarine != character.SelectedCharacter.Submarine) + { + targetSimPos += character.SelectedCharacter.Submarine.SimPosition; + targetSimPos -= character.Submarine.SimPosition; + } var body = Submarine.CheckVisibility(sourceSimPos, targetSimPos, ignoreSubs: true); if (body != null) { @@ -1553,8 +1574,8 @@ namespace Barotrauma Vector2 diff = ConvertUnits.ToSimUnits(targetLimb.WorldPosition - pullLimb.WorldPosition); - Vector2 targetAnchor = targetLimb.SimPosition; - float targetForce = 0.0f; + Vector2 targetAnchor; + float targetForce; pullLimb.PullJointEnabled = true; if (targetLimb.type == LimbType.Torso || targetLimb == target.AnimController.MainLimb) { @@ -1624,6 +1645,7 @@ namespace Barotrauma if (!target.AllowInput) { + target.AnimController.Stairs = Stairs; target.AnimController.IgnorePlatforms = IgnorePlatforms; target.AnimController.TargetMovement = TargetMovement; } @@ -1981,6 +2003,8 @@ namespace Barotrauma foreach (Limb limb in Limbs) { + if (limb.IsSevered) { continue; } + bool mirror = false; bool flipAngle = false; bool wrapAngle = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 20f981e2e..14de676c3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -55,7 +55,11 @@ namespace Barotrauma { if (limbs == null) { - DebugConsole.ThrowError("Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this)); + string errorMsg = "Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this); +#if DEBUG || UNSTABLE + errorMsg += '\n' + Environment.StackTrace; +#endif + DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce( "Ragdoll.Limbs:AccessRemoved", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, @@ -80,7 +84,7 @@ namespace Barotrauma frozen = value; Collider.FarseerBody.LinearDamping = frozen ? (1.5f / (float)Timing.Step) : 0.0f; - Collider.FarseerBody.AngularDamping = frozen ? (1.5f / (float)Timing.Step) : 0.0f; + Collider.FarseerBody.AngularDamping = frozen ? (1.5f / (float)Timing.Step) : PhysicsBody.DefaultAngularDamping; Collider.FarseerBody.IgnoreGravity = frozen; //Collider.PhysEnabled = !frozen; @@ -93,12 +97,17 @@ namespace Barotrauma private bool simplePhysicsEnabled; + public Character Character => character; protected Character character; protected float strongestImpact; private float splashSoundTimer; + //the ragdoll builds a "tolerance" to the flow force when being pushed by water. + //Allows sudden forces (breach, letting water through a door) to heavily push the character around while ensuring flowing water won't make the characters permanently stuck. + private float flowForceTolerance, flowStunTolerance; + //the movement speed of the ragdoll public Vector2 movement; //the target speed towards which movement is interpolated @@ -152,8 +161,8 @@ namespace Barotrauma } set { - if (value == colliderIndex || collider == null) return; - if (value >= collider.Count || value < 0) return; + if (value == colliderIndex || collider == null) { return; } + if (value >= collider.Count || value < 0) { return; } if (collider[colliderIndex].height < collider[value].height) { @@ -161,10 +170,9 @@ namespace Barotrauma pos1.Y -= collider[colliderIndex].height * ColliderHeightFromFloor; Vector2 pos2 = pos1; pos2.Y += collider[value].height * 1.1f; - if (GameMain.World.RayCast(pos1, pos2).Any(f => f.CollisionCategories.HasFlag(Physics.CollisionWall))) return; + if (GameMain.World.RayCast(pos1, pos2).Any(f => f.CollisionCategories.HasFlag(Physics.CollisionWall))) { return; } } - Vector2 pos = collider[colliderIndex].SimPosition; pos.Y -= collider[colliderIndex].height * 0.5f; pos.Y += collider[value].height * 0.5f; @@ -212,7 +220,7 @@ namespace Barotrauma mainLimb = torso ?? head; if (mainLimb == null) { - mainLimb = Limbs.FirstOrDefault(); + mainLimb = Limbs.FirstOrDefault(l => !l.IsSevered && !l.ignoreCollisions); } } return mainLimb; @@ -234,13 +242,13 @@ namespace Barotrauma get { return simplePhysicsEnabled; } set { - if (value == simplePhysicsEnabled) return; + if (value == simplePhysicsEnabled) { return; } simplePhysicsEnabled = value; foreach (Limb limb in Limbs) { - if (limb.IsSevered) continue; + if (limb.IsSevered) { continue; } if (limb.body == null) { DebugConsole.ThrowError("Limb has no body! (" + (character != null ? character.Name : "Unknown character") + ", " + limb.type.ToString()); @@ -292,8 +300,6 @@ namespace Barotrauma public float ImpactTolerance => RagdollParams.ImpactTolerance; public bool Draggable => RagdollParams.Draggable; public bool CanEnterSubmarine => RagdollParams.CanEnterSubmarine; - public bool CanAttackSubmarine => Limbs.Any(l => l.attack != null && l.attack.IsValidTarget(AttackTarget.Structure)); - public bool CanAttackCharacters => Limbs.Any(l => l.attack != null && l.attack.IsValidTarget(AttackTarget.Character)); public float Dir => dir == Direction.Left ? -1.0f : 1.0f; @@ -320,6 +326,7 @@ namespace Barotrauma Submarine currSubmarine = currentHull?.Submarine; foreach (Limb limb in Limbs) { + if (limb.IsSevered) { continue; } limb.body.Submarine = currSubmarine; } Collider.Submarine = currSubmarine; @@ -373,7 +380,7 @@ namespace Barotrauma foreach (var kvp in items) { int id = kvp.Key.ID; - // This can be the case if we manipulate the ragdoll in runtime (husk appendage, limb severance) + // This can be the case if we manipulate the ragdoll at runtime (husk appendage, limb removal in the character editor) if (id > limbs.Length - 1) { continue; } var limb = limbs[id]; var itemList = kvp.Value; @@ -633,10 +640,7 @@ namespace Barotrauma Vector2 colliderBottom = GetColliderBottom(); if (structure.IsPlatform) { - if (IgnorePlatforms) { return false; } - - //the collision is ignored if the lowest limb is under the platform - //if (lowestLimb==null || lowestLimb.Position.Y < structure.Rect.Y) return false; + if (IgnorePlatforms || currentHull == null) { return false; } if (colliderBottom.Y < ConvertUnits.ToSimUnits(structure.Rect.Y - 5)) { return false; } if (f1.Body.Position.Y < ConvertUnits.ToSimUnits(structure.Rect.Y - 5)) { return false; } @@ -719,12 +723,14 @@ namespace Barotrauma ImpactProjSpecific(impact, f1.Body); } - - public void SeverLimbJoint(LimbJoint limbJoint, bool playSound = true) + + private readonly List connectedLimbs = new List(); + private readonly List checkedJoints = new List(); + public bool SeverLimbJoint(LimbJoint limbJoint, bool playSound = true) { if (!limbJoint.CanBeSevered || limbJoint.IsSevered) { - return; + return false; } limbJoint.IsSevered = true; @@ -737,9 +743,8 @@ namespace Barotrauma limbJoint.LimbA.body.ApplyLinearImpulse(limbDiff * mass, (limbJoint.LimbA.SimPosition + limbJoint.LimbB.SimPosition) / 2.0f); limbJoint.LimbB.body.ApplyLinearImpulse(-limbDiff * mass, (limbJoint.LimbA.SimPosition + limbJoint.LimbB.SimPosition) / 2.0f); - List connectedLimbs = new List(); - List checkedJoints = new List(); - + connectedLimbs.Clear(); + checkedJoints.Clear(); GetConnectedLimbs(connectedLimbs, checkedJoints, MainLimb); foreach (Limb limb in Limbs) { @@ -748,11 +753,11 @@ namespace Barotrauma } SeverLimbJointProjSpecific(limbJoint, playSound: true); - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { GameMain.NetworkMember.CreateEntityEvent(character, new object[] { NetEntityEvent.Type.Status }); } + return true; } partial void SeverLimbJointProjSpecific(LimbJoint limbJoint, bool playSound); @@ -763,7 +768,7 @@ namespace Barotrauma foreach (LimbJoint joint in LimbJoints) { - if (joint.IsSevered || checkedJoints.Contains(joint)) continue; + if (joint.IsSevered || checkedJoints.Contains(joint)) { continue; } if (joint.LimbA == limb) { if (!connectedLimbs.Contains(joint.LimbB)) @@ -860,7 +865,7 @@ namespace Barotrauma { for (int i = 0; i < Limbs.Length; i++) { - if (Limbs[i] == null) continue; + if (Limbs[i] == null) { continue; } Limbs[i].PullJointEnabled = false; } } @@ -966,8 +971,8 @@ namespace Barotrauma else { if (character.Position.X < gap.Rect.X || character.Position.X > gap.Rect.Right) continue; - if (Math.Sign((gap.Rect.Y - gap.Rect.Height / 2) - (currentHull.Rect.Center.Y - currentHull.Rect.Height / 2)) != - Math.Sign(character.Position.Y - (currentHull.Rect.Center.Y - currentHull.Rect.Height / 2))) + if (Math.Sign((gap.Rect.Y - gap.Rect.Height / 2) - (currentHull.Rect.Y - currentHull.Rect.Height / 2)) != + Math.Sign(character.Position.Y - (currentHull.Rect.Y - currentHull.Rect.Height / 2))) { continue; } @@ -981,8 +986,8 @@ namespace Barotrauma { foreach (Limb limb in Limbs) { - if (limb.IsSevered) continue; - if (limb.body.FarseerBody.ContactList == null) continue; + if (limb.IsSevered) { continue; } + if (limb.body.FarseerBody.ContactList == null) { continue; } ContactEdge ce = limb.body.FarseerBody.ContactList; while (ce != null && ce.Contact != null) @@ -994,7 +999,7 @@ namespace Barotrauma foreach (Limb limb in Limbs) { - if (limb.IsSevered) continue; + if (limb.IsSevered) { continue; } limb.body.LinearVelocity += velocityChange; } @@ -1019,15 +1024,15 @@ namespace Barotrauma Category collisionCategory = (IgnorePlatforms) ? wall | Physics.CollisionProjectile | Physics.CollisionStairs : wall | Physics.CollisionProjectile | Physics.CollisionPlatform | Physics.CollisionStairs; - - if (collisionCategory == prevCollisionCategory) return; + + if (collisionCategory == prevCollisionCategory) { return; } prevCollisionCategory = collisionCategory; Collider.CollidesWith = collisionCategory | Physics.CollisionItemBlocking; foreach (Limb limb in Limbs) { - if (limb.ignoreCollisions || limb.IsSevered) continue; + if (limb.ignoreCollisions || limb.IsSevered) { continue; } try { @@ -1080,8 +1085,6 @@ namespace Barotrauma CheckDistFromCollider(); UpdateCollisionCategories(); - Vector2 flowForce = Vector2.Zero; - FindHull(); PreventOutsideCollision(); @@ -1103,10 +1106,7 @@ namespace Barotrauma } else { - flowForce = GetFlowForce(); - headInWater = false; - inWater = false; if (currentHull.WaterVolume > currentHull.Volume * 0.95f) { @@ -1128,7 +1128,7 @@ namespace Barotrauma if (lowerHull != null) floorY = ConvertUnits.ToSimUnits(lowerHull.Rect.Y - lowerHull.Rect.Height); } } - float standHeight = + float standHeight = HeadPosition.HasValue ? HeadPosition.Value : TorsoPosition.HasValue ? TorsoPosition.Value : Collider.GetMaxExtent() * 0.5f; @@ -1139,10 +1139,7 @@ namespace Barotrauma } } - if (flowForce.LengthSquared() > 0.001f) - { - Collider.ApplyForce(flowForce, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); - } + UpdateHullFlowForces(deltaTime); if (currentHull == null || currentHull.WaterVolume > currentHull.Volume * 0.95f || @@ -1151,7 +1148,6 @@ namespace Barotrauma Collider.ApplyWaterForces(); } - foreach (Limb limb in Limbs) { //find the room which the limb is in @@ -1176,14 +1172,7 @@ namespace Barotrauma if (limb.Position.Y < limbHull.Surface) { limb.inWater = true; - - if (flowForce.LengthSquared() > 0.001f) - { - limb.body.ApplyForce(flowForce, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); - } - surfaceY = limbHull.Surface; - if (limb.type == LimbType.Head) { headInWater = true; @@ -1228,6 +1217,8 @@ namespace Barotrauma private void CheckBodyInRest(float deltaTime) { + if (SimplePhysicsEnabled) { return; } + if (InWater || Collider.LinearVelocity.LengthSquared() > 0.01f || character.SelectedBy != null || !character.IsDead) { bodyInRestTimer = 0.0f; @@ -1253,6 +1244,12 @@ namespace Barotrauma private int validityResets; private bool CheckValidity() { + if (limbs == null) + { + DebugConsole.ThrowError("Attempted to check the validity of a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this)); + Invalid = true; + return false; + } bool isColliderValid = CheckValidity(Collider); bool limbsValid = true; foreach (Limb limb in limbs) @@ -1275,8 +1272,8 @@ namespace Barotrauma Collider.SetTransform(Vector2.Zero, 0.0f); foreach (Limb limb in Limbs) { - limb.body.SetTransform(Collider.SimPosition, 0.0f); - limb.body.ResetDynamics(); + limb.body?.SetTransform(Collider.SimPosition, 0.0f); + limb.body?.ResetDynamics(); } Frozen = true; } @@ -1350,6 +1347,74 @@ namespace Barotrauma partial void Splash(Limb limb, Hull limbHull); + private void UpdateHullFlowForces(float deltaTime) + { + if (currentHull == null) { return; } + + const float StunForceThreshold = 5.0f; + const float StunDuration = 0.5f; + const float ToleranceIncreaseSpeed = 5.0f; + const float ToleranceDecreaseSpeed = 1.0f; + + //how much distance to a gap affects the force it exerts on the character + const float DistanceFactor = 0.5f; + const float ForceMultiplier = 0.035f; + + Vector2 flowForce = Vector2.Zero; + foreach (Gap gap in Gap.GapList) + { + if (gap.Open <= 0.0f || !gap.linkedTo.Contains(currentHull) || gap.LerpedFlowForce.LengthSquared() < 0.01f) { continue; } + float dist = Vector2.Distance(MainLimb.WorldPosition, gap.WorldPosition) * DistanceFactor; + flowForce += Vector2.Normalize(gap.LerpedFlowForce) * (Math.Max(gap.LerpedFlowForce.Length() - dist, 0.0f) * ForceMultiplier); + } + + //throwing conscious/moving characters around takes more force -> double the flow force + if (character.CanMove) { flowForce *= 2.0f; } + + float flowForceMagnitude = flowForce.Length(); + float limbMultipier = limbs.Count(l => l.inWater) / (float)limbs.Length; + //if the force strong enough, stun the character to let it get thrown around by the water + if ((flowForceMagnitude * limbMultipier) - flowStunTolerance > StunForceThreshold) + { + character.Stun = Math.Max(character.Stun, StunDuration); + flowStunTolerance = Math.Max(flowStunTolerance, flowForceMagnitude); + } + + if (character == Character.Controlled && inWater && Screen.Selected?.Cam != null) + { + float shakeStrength = Math.Min(flowForceMagnitude / 10.0f, 5.0f) * limbMultipier; + Screen.Selected.Cam.Shake = Math.Max(Screen.Selected.Cam.Shake, shakeStrength); + } + + if (flowForceMagnitude > 0.0001f) + { + flowForce = Vector2.Normalize(flowForce) * Math.Max(flowForceMagnitude - flowForceTolerance, 0.0f); + } + + if (flowForceTolerance <= flowForceMagnitude * 1.5f && inWater) + { + //build up "tolerance" to the flow force + //ensures the character won't get permanently stuck by forces, while allowing sudden changes in flow to push the character hard + flowForceTolerance += deltaTime * ToleranceIncreaseSpeed; + flowStunTolerance = Math.Max(flowStunTolerance, flowForceTolerance); + } + else + { + flowForceTolerance = Math.Max(flowForceTolerance - deltaTime * ToleranceDecreaseSpeed, 0.0f); + flowStunTolerance = Math.Max(flowStunTolerance - deltaTime * ToleranceDecreaseSpeed, 0.0f); + } + + if (flowForce.LengthSquared() > 0.001f) + { + Collider.ApplyForce(flowForce, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + foreach (Limb limb in limbs) + { + if (!limb.inWater) { continue; } + limb.body.ApplyForce(flowForce, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + } + } + } + private void RefreshFloorY(Limb refLimb = null, bool ignoreStairs = false) { PhysicsBody refBody = refLimb == null ? Collider : refLimb.body; @@ -1487,7 +1552,7 @@ namespace Barotrauma foreach (Limb limb in Limbs) { - if (limb.IsSevered) continue; + if (limb.IsSevered) { continue; } //check visibility from the new position of the collider to the new position of this limb Vector2 movePos = limb.SimPosition + limbMoveAmount; @@ -1551,7 +1616,7 @@ namespace Barotrauma Vector2 forceDir = diff / (float)Math.Sqrt(distSqrd); foreach (Limb limb in Limbs) { - if (limb.IsSevered) continue; + if (limb.IsSevered) { continue; } limb.body.CollidesWith = Physics.CollisionNone; limb.body.ApplyForce(forceDir * limb.Mass * 10.0f, maxVelocity: 10.0f); } @@ -1597,28 +1662,37 @@ namespace Barotrauma UpdateNetPlayerPositionProjSpecific(deltaTime, lowestSubPos); } - private Vector2 GetFlowForce() - { - Vector2 limbPos = Limbs[0].Position; - - Vector2 force = Vector2.Zero; - foreach (Gap gap in Gap.GapList) - { - if (gap.Open <= 0.0f || gap.FlowTargetHull != currentHull || gap.LerpedFlowForce.LengthSquared() < 0.01f) continue; - - Vector2 gapPos = gap.SimPosition; - float dist = Vector2.Distance(limbPos, gapPos); - force += Vector2.Normalize(gap.LerpedFlowForce) * (Math.Max(gap.LerpedFlowForce.Length() - dist, 0.0f) / 500.0f); - } - return force; - } - /// /// Note that if there are multiple limbs of the same type, only the first of them is found in the dictionary. /// - public Limb GetLimb(LimbType limbType) + public Limb GetLimb(LimbType limbType, bool excludeSevered = true) { - limbDictionary.TryGetValue(limbType, out Limb limb); + Limb limb = null; + if (HasMultipleLimbsOfSameType) + { + for (int i = 0; i < 10; i++) + { + limbDictionary.TryGetValue(limbType, out limb); + if (limb == null) + { + // No limbs found + break; + } + if (!excludeSevered || !limb.IsSevered) + { + // Found a valid limb + break; + } + } + } + else + { + limbDictionary.TryGetValue(limbType, out limb); + } + if (excludeSevered && limb != null && limb.IsSevered) + { + limb = null; + } return limb; } @@ -1661,10 +1735,15 @@ namespace Barotrauma Limb lowestLimb = null; foreach (Limb limb in Limbs) { + if (limb.IsSevered) { continue; } if (lowestLimb == null) + { lowestLimb = limb; + } else if (limb.SimPosition.Y < lowestLimb.SimPosition.Y) + { lowestLimb = limb; + } } return lowestLimb; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index c722ecb4b..4729ea027 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -260,7 +260,7 @@ namespace Barotrauma return totalDamage; } - public Attack(float damage, float bleedingDamage, float burnDamage, float structureDamage, float range = 0.0f) + public Attack(float damage, float bleedingDamage, float burnDamage, float structureDamage, float itemDamage, float range = 0.0f) { if (damage > 0.0f) Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(damage), null); if (bleedingDamage > 0.0f) Afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamage), null); @@ -269,6 +269,7 @@ namespace Barotrauma Range = range; DamageRange = range; StructureDamage = structureDamage; + ItemDamage = itemDamage; } public Attack(XElement element, string parentDebugName) @@ -315,11 +316,6 @@ namespace Barotrauma continue; } } - - //float afflictionStrength = subElement.GetAttributeFloat(1.0f, "amount", "strength"); - //var affliction = afflictionPrefab.Instantiate(afflictionStrength); - //Afflictions.Add(affliction, subElement); - break; case "conditional": foreach (XAttribute attribute in subElement.Attributes()) @@ -346,14 +342,18 @@ namespace Barotrauma afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.Equals(afflictionIdentifier, System.StringComparison.OrdinalIgnoreCase)); if (afflictionPrefab != null) { - float afflictionStrength = subElement.GetAttributeFloat(1.0f, "amount", "strength"); - affliction = afflictionPrefab.Instantiate(afflictionStrength); + affliction = afflictionPrefab.Instantiate(0.0f); } else { affliction = new Affliction(null, 0); } affliction.Deserialize(subElement); + //backwards compatibility + if (subElement.Attribute("amount") != null && subElement.Attribute("strength") == null) + { + affliction.Strength = subElement.GetAttributeFloat("amount", 0.0f); + } // add the affliction anyway, so that it can be shown in the editor. Afflictions.Add(affliction, subElement); } @@ -515,10 +515,10 @@ namespace Barotrauma public void SetCoolDown() { float randomFraction = CoolDown * CoolDownRandomFactor; - CurrentRandomCoolDown = MathHelper.Lerp(-randomFraction, randomFraction, Rand.Value(Rand.RandSync.Server)); + CurrentRandomCoolDown = MathHelper.Lerp(-randomFraction, randomFraction, Rand.Value()); CoolDownTimer = CoolDown + CurrentRandomCoolDown; randomFraction = SecondaryCoolDown * CoolDownRandomFactor; - SecondaryCoolDownTimer = SecondaryCoolDown + MathHelper.Lerp(-randomFraction, randomFraction, Rand.Value(Rand.RandSync.Server)); + SecondaryCoolDownTimer = SecondaryCoolDown + MathHelper.Lerp(-randomFraction, randomFraction, Rand.Value()); } public void ResetCoolDown() @@ -571,18 +571,14 @@ namespace Barotrauma public bool IsValidTarget(AttackTarget targetType) => TargetType == AttackTarget.Any || TargetType == targetType; - public bool IsValidTarget(Entity target) + public bool IsValidTarget(IDamageable target) { - switch (TargetType) + return TargetType switch { - case AttackTarget.Character: - return target is Character; - case AttackTarget.Structure: - return !(target is Character); - case AttackTarget.Any: - default: - return true; - } + AttackTarget.Character => target is Character, + AttackTarget.Structure => !(target is Character), + _ => true, + }; } public Vector2 CalculateAttackPhase(TransitionMode easing = TransitionMode.Linear) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index fc0f34d0c..53cd51fdf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -3,7 +3,7 @@ using FarseerPhysics; using FarseerPhysics.Dynamics.Joints; using Microsoft.Xna.Framework; using System; -using System.IO; +using Barotrauma.IO; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; @@ -43,6 +43,7 @@ namespace Barotrauma foreach (Limb limb in AnimController.Limbs) { + if (limb.IsSevered) { continue; } if (limb.body != null) { limb.body.Enabled = enabled; @@ -59,6 +60,7 @@ namespace Barotrauma public bool IsRemotePlayer; public bool IsPlayer => Controlled == this || IsRemotePlayer; + public bool IsBot => !IsPlayer && AIController is HumanAIController humanAI && humanAI.Enabled; public readonly Dictionary Properties; public Dictionary SerializableProperties @@ -234,6 +236,7 @@ namespace Barotrauma { get { + if (info != null && !string.IsNullOrWhiteSpace(info.Name)) { return info.Name; } var displayName = Params.DisplayName; if (string.IsNullOrWhiteSpace(displayName)) { @@ -601,7 +604,9 @@ namespace Barotrauma { errorMsg += " AnimController.Collider == null"; } - +#if DEBUG || UNSTABLE + errorMsg += '\n' + Environment.StackTrace; +#endif DebugConsole.NewMessage(errorMsg, Color.Red); GameAnalyticsManager.AddErrorEventOnce( "Character.SimPosition:AccessRemoved", @@ -1137,22 +1142,76 @@ namespace Barotrauma greatestNegativeSpeedMultiplier = 1f; } + /// + /// Speed reduction from the current limb specific damage. Min 0, max 1. + /// + public float GetTemporarySpeedReduction() + { + float reduction = 0; + reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.RightFoot, excludeSevered: false), reduction); + reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.LeftFoot, excludeSevered: false), reduction); + reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.RightHand, excludeSevered: false), reduction); + reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.LeftHand, excludeSevered: false), reduction); + int totalTailLimbs = 0; + int destroyedTailLimbs = 0; + foreach (var limb in AnimController.Limbs) + { + if (limb.type == LimbType.Tail) + { + totalTailLimbs++; + if (limb.IsSevered) + { + destroyedTailLimbs++; + } + } + } + if (destroyedTailLimbs > 0) + { + reduction += MathHelper.Lerp(0, AnimController.InWater ? 1f : 0.5f, (float)destroyedTailLimbs / totalTailLimbs); + } + return Math.Clamp(reduction, 0, 1f); + } + + private float CalculateMovementPenalty(Limb limb, float sum, float max = 0.4f) + { + if (limb != null) + { + sum += MathHelper.Lerp(0, max, CharacterHealth.GetLimbDamage(limb)); + } + return Math.Clamp(sum, 0, 1f); + } + + public float GetRightHandPenalty() => CalculateMovementPenalty(AnimController.GetLimb(LimbType.RightHand, excludeSevered: false), 0, max: 1); + public float GetLeftHandPenalty() => CalculateMovementPenalty(AnimController.GetLimb(LimbType.LeftHand, excludeSevered: false), 0, max: 1); + + public float GetLegPenalty(float startSum = 0) + { + float sum = startSum; + foreach (var limb in AnimController.Limbs) + { + switch (limb.type) + { + case LimbType.RightFoot: + case LimbType.LeftFoot: + sum += CalculateMovementPenalty(limb, sum, max: 0.5f); + break; + } + } + return Math.Clamp(sum, 0, 1f); + } + public float ApplyTemporarySpeedLimits(float speed) { - var leftFoot = AnimController.GetLimb(LimbType.LeftFoot); - if (leftFoot != null) + float max; + if (AnimController is HumanoidAnimController) { - float footAfflictionStrength = CharacterHealth.GetAfflictionStrength("damage", leftFoot, true); - speed *= MathHelper.Lerp(1.0f, 0.4f, MathHelper.Clamp(footAfflictionStrength / 80.0f, 0.0f, 1.0f)); + max = AnimController.InWater ? 0.5f : 0.7f; } - - var rightFoot = AnimController.GetLimb(LimbType.RightFoot); - if (rightFoot != null) + else { - float footAfflictionStrength = CharacterHealth.GetAfflictionStrength("damage", rightFoot, true); - speed *= MathHelper.Lerp(1.0f, 0.4f, MathHelper.Clamp(footAfflictionStrength / 80.0f, 0.0f, 1.0f)); + max = AnimController.InWater ? 0.9f : 0.5f; } - + speed *= 1f - MathHelper.Lerp(0, max, GetTemporarySpeedReduction()); return speed; } @@ -1251,60 +1310,74 @@ namespace Barotrauma { attackCoolDown -= deltaTime; } - else if (IsKeyDown(InputType.Attack)) + else if (IsKeyDown(InputType.Attack) && (IsRemotePlayer || Controlled == this || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient))) { + Vector2 attackPos = SimPosition + ConvertUnits.ToSimUnits(cursorPosition - Position); + List ignoredBodies = AnimController.Limbs.Select(l => l.body.FarseerBody).ToList(); + ignoredBodies.Add(AnimController.Collider.FarseerBody); + + var body = Submarine.PickBody( + SimPosition, + attackPos, + ignoredBodies, + Physics.CollisionCharacter | Physics.CollisionWall); + + IDamageable attackTarget = null; + if (body != null) + { + attackPos = Submarine.LastPickedPosition; + + if (body.UserData is Submarine sub) + { + body = Submarine.PickBody( + SimPosition - ((Submarine)body.UserData).SimPosition, + attackPos - ((Submarine)body.UserData).SimPosition, + ignoredBodies, + Physics.CollisionWall); + + if (body != null) + { + attackPos = Submarine.LastPickedPosition + sub.SimPosition; + attackTarget = body.UserData as IDamageable; + } + } + else + { + if (body.UserData is IDamageable) + { + attackTarget = (IDamageable)body.UserData; + } + else if (body.UserData is Limb) + { + attackTarget = ((Limb)body.UserData).character; + } + } + } var currentContexts = GetAttackContexts(); - var validLimbs = AnimController.Limbs.Where(l => !l.IsSevered && !l.IsStuck && l.attack != null && l.attack.IsValidContext(currentContexts)); + var validLimbs = AnimController.Limbs.Where(l => + { + if (l.IsSevered || l.IsStuck) { return false; } + var attack = l.attack; + if (attack == null) { return false; } + if (attack.CoolDownTimer > 0) { return false; } + if (!attack.IsValidContext(currentContexts)) { return false; } + if (attackTarget != null) + { + if (!attack.IsValidTarget(attackTarget)) { return false; } + if (attackTarget is ISerializableEntity se && attackTarget is Character) + { + if (attack.Conditionals.Any(c => !c.Matches(se))) { return false; } + } + } + if (attack.Conditionals.Any(c => c.TargetSelf && !c.Matches(this))) { return false; } + return true; + }); var sortedLimbs = validLimbs.OrderBy(l => Vector2.DistanceSquared(ConvertUnits.ToDisplayUnits(l.SimPosition), cursorPosition)); // Select closest var attackLimb = sortedLimbs.FirstOrDefault(); if (attackLimb != null) { - Vector2 attackPos = attackLimb.SimPosition + Vector2.Normalize(cursorPosition - attackLimb.Position) * ConvertUnits.ToSimUnits(attackLimb.attack.Range); - - List ignoredBodies = AnimController.Limbs.Select(l => l.body.FarseerBody).ToList(); - ignoredBodies.Add(AnimController.Collider.FarseerBody); - - var body = Submarine.PickBody( - attackLimb.SimPosition, - attackPos, - ignoredBodies, - Physics.CollisionCharacter | Physics.CollisionWall); - - IDamageable attackTarget = null; - if (body != null) - { - attackPos = Submarine.LastPickedPosition; - - if (body.UserData is Submarine sub) - { - body = Submarine.PickBody( - attackLimb.SimPosition - ((Submarine)body.UserData).SimPosition, - attackPos - ((Submarine)body.UserData).SimPosition, - ignoredBodies, - Physics.CollisionWall); - - if (body != null) - { - attackPos = Submarine.LastPickedPosition + sub.SimPosition; - attackTarget = body.UserData as IDamageable; - } - } - else - { - if (body.UserData is IDamageable) - { - attackTarget = (IDamageable)body.UserData; - } - else if (body.UserData is Limb) - { - attackTarget = ((Limb)body.UserData).character; - } - } - } - attackLimb.UpdateAttack(deltaTime, attackPos, attackTarget, out AttackResult attackResult); - if (!attackLimb.attack.IsRunning) { attackCoolDown = 1.0f; @@ -1391,22 +1464,23 @@ namespace Barotrauma { Limb selfLimb = AnimController.GetLimb(LimbType.Head); if (selfLimb == null) { selfLimb = AnimController.GetLimb(LimbType.Torso); } - if (selfLimb == null) { selfLimb = AnimController.Limbs.FirstOrDefault(); } + if (selfLimb == null) { selfLimb = AnimController.MainLimb; } return selfLimb; } public bool CanSeeTarget(ISpatialEntity target, Limb seeingLimb = null) { - seeingLimb = seeingLimb ?? GetSeeingLimb(); + seeingLimb ??= GetSeeingLimb(); if (seeingLimb == null) { return false; } + ISpatialEntity seeingEntity = AnimController.SimplePhysicsEnabled ? this : seeingLimb as ISpatialEntity; // TODO: Could we just use the method below? If not, let's refactor it so that we can. - Vector2 diff = ConvertUnits.ToSimUnits(target.WorldPosition - seeingLimb.WorldPosition); + Vector2 diff = ConvertUnits.ToSimUnits(target.WorldPosition - seeingEntity.WorldPosition); Body closestBody; //both inside the same sub (or both outside) //OR the we're inside, the other character outside if (target.Submarine == Submarine || target.Submarine == null) { - closestBody = Submarine.CheckVisibility(seeingLimb.SimPosition, seeingLimb.SimPosition + diff); + closestBody = Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff); } //we're outside, the other character inside else if (Submarine == null) @@ -1416,7 +1490,7 @@ namespace Barotrauma //both inside different subs else { - closestBody = Submarine.CheckVisibility(seeingLimb.SimPosition, seeingLimb.SimPosition + diff); + closestBody = Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff); if (!IsBlocking(closestBody)) { closestBody = Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff); @@ -1433,10 +1507,11 @@ namespace Barotrauma } else if (body.UserData is Item item && item != target) { + // TODO: The door collider should be disabled, so this check is probably unnecessary. var door = item.GetComponent(); if (door != null) { - return !door.IsOpen; + return !door.IsOpen && !door.IsBroken; } } return false; @@ -1463,7 +1538,7 @@ namespace Barotrauma Structure wall = closestBody.UserData as Structure; Item item = closestBody.UserData as Item; Door door = item?.GetComponent(); - return (wall == null || !wall.CastShadow) && (door == null || door.IsOpen); + return (wall == null || !wall.CastShadow) && (door == null || door.IsOpen || door.IsBroken); } public bool HasItem(Item item, bool requireEquipped = false) => requireEquipped ? HasEquippedItem(item) : item.IsOwnedBy(this); @@ -1561,7 +1636,7 @@ namespace Barotrauma /// /// 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. + /// Also ignores non-interactable items and 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. /// @@ -1578,12 +1653,17 @@ namespace Barotrauma { itemIndex++; var item = Item.ItemList[itemIndex]; + if (item.NonInteractable) { continue; } 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 (Submarine != null) + { + if (item.Submarine.Info.Type != Submarine.Info.Type) { continue; } + if (!Submarine.IsEntityFoundOnThisSub(item, true)) { 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) @@ -1593,8 +1673,8 @@ namespace Barotrauma if (IsItemTakenBySomeoneElse(item)) { continue; } float itemPriority = customPriorityFunction != null ? customPriorityFunction(item) : 1; if (itemPriority <= 0) { continue; } - Item rootContainer = item.GetRootContainer(); - Vector2 itemPos = (rootContainer ?? item).WorldPosition; + Entity rootInventoryOwner = item.GetRootInventoryOwner(); + Vector2 itemPos = (rootInventoryOwner ?? 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; @@ -1612,13 +1692,16 @@ namespace Barotrauma 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) + public bool CanInteractWith(Character c, float maxDist = 200.0f, bool checkVisibility = true, bool skipDistanceCheck = false) { - if (c == this || Removed || !c.Enabled || !c.CanBeSelected) return false; - if (!c.CharacterHealth.UseHealthWindow && !c.CanBeDragged && c.onCustomInteract == null) return false; + if (c == this || Removed || !c.Enabled || !c.CanBeSelected) { return false; } + if (!c.CharacterHealth.UseHealthWindow && !c.CanBeDragged && c.onCustomInteract == null) { return false; } - maxDist = ConvertUnits.ToSimUnits(maxDist); - if (Vector2.DistanceSquared(SimPosition, c.SimPosition) > maxDist * maxDist) return false; + if (!skipDistanceCheck) + { + maxDist = ConvertUnits.ToSimUnits(maxDist); + if (Vector2.DistanceSquared(SimPosition, c.SimPosition) > maxDist * maxDist) { return false; } + } return checkVisibility ? CanSeeCharacter(c) : true; } @@ -1824,13 +1907,17 @@ namespace Barotrauma if (findFocusedTimer <= 0.0f || Screen.Selected == GameMain.SubEditorScreen) { FocusedCharacter = CanInteract ? FindCharacterAtPosition(mouseSimPos) : null; + if (FocusedCharacter != null && !CanSeeCharacter(FocusedCharacter)) { FocusedCharacter = null; } float aimAssist = GameMain.Config.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f); if (SelectedItems.Any(it => it?.GetComponent()?.IsActive ?? false)) { //disable aim assist when rewiring to make it harder to accidentally select items when adding wire nodes aimAssist = 0.0f; } - focusedItem = CanInteract ? FindItemAtPosition(mouseSimPos, aimAssist) : null; + + var item = FindItemAtPosition(mouseSimPos, aimAssist); + + focusedItem = CanInteract ? item : null; findFocusedTimer = 0.05f; } } @@ -1964,7 +2051,7 @@ namespace Barotrauma if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { //disable AI characters that are far away from all clients and the host's character and not controlled by anyone - if (c == Controlled || c.IsRemotePlayer) + if (c.IsPlayer || (c.IsBot && !c.IsDead)) { c.Enabled = true; } @@ -2182,11 +2269,12 @@ namespace Barotrauma lowPassMultiplier = MathHelper.Lerp(lowPassMultiplier, 1.0f, 0.1f); //ragdoll button - if (IsRagdolled) + if (IsRagdolled || !CanMove) { - if (AnimController is HumanoidAnimController) ((HumanoidAnimController)AnimController).Crouching = false; - /*if(GameMain.Server != null) - GameMain.Server.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.Status });*/ + if (AnimController is HumanoidAnimController) + { + ((HumanoidAnimController)AnimController).Crouching = false; + } AnimController.ResetPullJoints(); SelectedConstruction = null; return; @@ -2209,7 +2297,7 @@ namespace Barotrauma SelectedConstruction = null; } - if (!IsDead) LockHands = false; + if (!IsDead) { LockHands = false; } } partial void UpdateControlled(float deltaTime, Camera cam); @@ -2295,9 +2383,12 @@ namespace Barotrauma if (!IsDead) { return; } - if (Submarine != null && CharacterList.Count(c => c.IsDead && c.Submarine == Submarine) < GameMain.Config.CorpsesPerSubDespawnThreshold) + int subCorpseCount = 0; + + if (Submarine != null) { - return; + subCorpseCount = CharacterList.Count(c => c.IsDead && c.Submarine == Submarine); + if (subCorpseCount < GameMain.Config.CorpsesPerSubDespawnThreshold) { return; } } float distToClosestPlayer = GetDistanceToClosestPlayer(); @@ -2306,8 +2397,20 @@ namespace Barotrauma //despawn in 1 minute if very far from all human players despawnTimer = Math.Max(despawnTimer, GameMain.Config.CorpseDespawnDelay - 60.0f); } + + float despawnPriority = 1.0f; + if (subCorpseCount > GameMain.Config.CorpsesPerSubDespawnThreshold) + { + //despawn faster if there are lots of corpses in the sub (twice as many as the threshold -> despawn twice as fast) + despawnPriority += (subCorpseCount - GameMain.Config.CorpsesPerSubDespawnThreshold) / (float)GameMain.Config.CorpsesPerSubDespawnThreshold; + } + if (AIController is EnemyAIController) + { + //enemies despawn faster + despawnPriority *= 2.0f; + } - despawnTimer += deltaTime; + despawnTimer += deltaTime * despawnPriority; if (despawnTimer < GameMain.Config.CorpseDespawnDelay) { return; } if (IsHuman) @@ -2416,6 +2519,14 @@ namespace Barotrauma //set the character order only if the character is close enough to hear the message if (orderGiver != null && !CanHearCharacter(orderGiver)) { return; } + // If there's another character operating the same device, make them dismiss themself + if (order != null && order.Category == OrderCategory.Operate) + { + CharacterList.FindAll(c => c != this && c.TeamID == TeamID && c.CurrentOrder is Order characterOrder && characterOrder.Category == OrderCategory.Operate && + characterOrder.Identifier.Equals(order.Identifier) && characterOrder.TargetEntity == order.TargetEntity)? + .ForEach(c => c.SetOrder(Order.GetPrefab("dismissed"), null, c, speak: true)); + } + if (AIController is HumanAIController humanAI) { humanAI.SetOrder(order, orderOption, orderGiver, speak); @@ -2426,6 +2537,11 @@ namespace Barotrauma CurrentOrderOption = orderOption; } + /// + /// Reset order data so it doesn't carry into further rounds, as the AI is "recreated" always in between rounds anyway. + /// + public void ResetCurrentOrder() => Info?.ResetCurrentOrder(); + private readonly List aiChatMessageQueue = new List(); //key = identifier, value = time the message was sent @@ -2575,7 +2691,7 @@ namespace Barotrauma if (attacker is Character attackingCharacter && attackingCharacter.AIController == null) { StringBuilder sb = new StringBuilder(); - sb.Append(LogName + " attacked by " + attackingCharacter.LogName + "."); + sb.Append(GameServer.CharacterLogName(this) + " attacked by " + GameServer.CharacterLogName(attackingCharacter) + "."); if (attackResult.Afflictions != null) { foreach (Affliction affliction in attackResult.Afflictions) @@ -2588,40 +2704,62 @@ namespace Barotrauma } #endif - bool isNotClient = GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient; - - TrySeverLimbJoints(limbHit, attack.SeverLimbsProbability); + TrySeverLimbJoints(limbHit, attack.SeverLimbsProbability, attackResult.Damage); return attackResult; } - public void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability) + public void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability, float damage) { - bool isNotClient = GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient; - - if (isNotClient && - IsDead && Rand.Range(0.0f, 1.0f) < severLimbsProbability) + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } +#if DEBUG + if (targetLimb.character != this) { - foreach (LimbJoint joint in AnimController.LimbJoints) - { - if (joint.CanBeSevered && (joint.LimbA == targetLimb || joint.LimbB == targetLimb)) - { -#if CLIENT - CurrentHull?.AddDecal("blood", WorldPosition, Rand.Range(0.5f, 1.5f)); + DebugConsole.ThrowError($"{Name} is attempting to sever joints of {targetLimb.character.Name}!"); + return; + } #endif - AnimController.SeverLimbJoint(joint); - - if (joint.LimbA == targetLimb) - { - joint.LimbB.body.LinearVelocity += targetLimb.LinearVelocity * 0.5f; - } - else - { - joint.LimbA.body.LinearVelocity += targetLimb.LinearVelocity * 0.5f; - } + if (!IsDead && !targetLimb.CanBeSeveredAlive) { return; } + if (damage < targetLimb.Params.MinSeveranceDamage) { return; } + bool wasSevered = false; + float random = Rand.Value(); + foreach (LimbJoint joint in AnimController.LimbJoints) + { + if (!joint.CanBeSevered) { continue; } + if (joint.LimbA != targetLimb && joint.LimbB != targetLimb) { continue; } + float probability = severLimbsProbability; + if (!IsDead) + { + probability *= joint.Params.SeveranceProbabilityModifier; + } + if (probability <= 0) { continue; } + if (random > probability) { continue; } + bool severed = AnimController.SeverLimbJoint(joint); + if (!wasSevered) + { + wasSevered = severed; + } + if (severed) + { + if (joint.LimbA == targetLimb) + { + joint.LimbB.body.LinearVelocity += targetLimb.LinearVelocity * 0.5f; + } + else + { + joint.LimbA.body.LinearVelocity += targetLimb.LinearVelocity * 0.5f; } } } + if (wasSevered) + { + if (targetLimb.character.AIController is EnemyAIController enemyAI) + { + enemyAI.ReevaluateAttacks(); + } + ApplyStatusEffects(ActionType.OnSevered, 1.0f); + targetLimb.ApplyStatusEffects(ActionType.OnSevered, 1.0f); + } } public AttackResult AddDamage(Vector2 worldPosition, IEnumerable afflictions, float stun, bool playSound, float attackImpulse = 0.0f, Character attacker = null) @@ -2700,10 +2838,15 @@ namespace Barotrauma Vector2 simPos = hitLimb.SimPosition + ConvertUnits.ToSimUnits(dir); AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound); CharacterHealth.ApplyDamage(hitLimb, attackResult); + ApplyStatusEffects(ActionType.OnDamaged, 1.0f); + if (attackResult.Damage > 0) + { + hitLimb.ApplyStatusEffects(ActionType.OnDamaged, 1.0f); + } if (attacker != this) { OnAttacked?.Invoke(attacker, attackResult); - OnAttackedProjSpecific(attacker, attackResult); + OnAttackedProjSpecific(attacker, attackResult, stun); if (!wasDead) { TryAdjustAttackerSkill(attacker, -attackResult.Damage); @@ -2718,7 +2861,7 @@ namespace Barotrauma return attackResult; } - partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult); + partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult, float stun); public void TryAdjustAttackerSkill(Character attacker, float healthChange) { @@ -2760,6 +2903,7 @@ namespace Barotrauma } } + private readonly List targets = new List(); public void ApplyStatusEffects(ActionType actionType, float deltaTime) { foreach (StatusEffect statusEffect in statusEffects) @@ -2768,30 +2912,60 @@ namespace Barotrauma if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) || statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { - var targets = new List(); + targets.Clear(); statusEffect.GetNearbyTargets(WorldPosition, targets); statusEffect.Apply(ActionType.OnActive, deltaTime, this, targets); } else { statusEffect.Apply(actionType, deltaTime, this, this); + if (statusEffect.targetLimbs != null) + { + foreach (var limbType in statusEffect.targetLimbs) + { + if (statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs)) + { + // Target all matching limbs + foreach (var limb in AnimController.Limbs) + { + if (limb.IsSevered) { continue; } + if (limb.type == limbType) + { + statusEffect.Apply(actionType, deltaTime, this, limb); + } + } + } + else if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb)) + { + // Target just the first matching limb + Limb limb = AnimController.GetLimb(limbType); + statusEffect.Apply(actionType, deltaTime, this, limb); + } + } + } } } + if (actionType != ActionType.OnDamaged && actionType != ActionType.OnSevered) + { + // OnDamaged is called only for the limb that is hit. + AnimController.Limbs.ForEach(l => l.ApplyStatusEffects(actionType, deltaTime)); + } } private void Implode(bool isNetworkMessage = false) { - if (CharacterHealth.Unkillable) { return; } + if (CharacterHealth.Unkillable || IsDead) { return; } if (!isNetworkMessage) { - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) return; + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } } - Kill(CauseOfDeathType.Pressure, null, isNetworkMessage); - CharacterHealth.PressureAffliction.Strength = CharacterHealth.PressureAffliction.Prefab.MaxStrength; - CharacterHealth.SetAllDamage(200.0f, 0.0f, 0.0f); - BreakJoints(); + CharacterHealth.ApplyAffliction(null, new Affliction(AfflictionPrefab.Pressure, AfflictionPrefab.Pressure.MaxStrength)); + if (IsDead) + { + BreakJoints(); + } } public void BreakJoints() @@ -2799,6 +2973,7 @@ namespace Barotrauma Vector2 centerOfMass = AnimController.GetCenterOfMass(); foreach (Limb limb in AnimController.Limbs) { + if (limb.IsSevered) { continue; } limb.AddDamage(limb.SimPosition, 500.0f, 0.0f, 0.0f, false); Vector2 diff = centerOfMass - limb.SimPosition; @@ -2866,7 +3041,10 @@ namespace Barotrauma causeOfDeathAffliction?.Source ?? LastAttacker, LastDamageSource); OnDeath?.Invoke(this, CauseOfDeath); - SteamAchievementManager.OnCharacterKilled(this, CauseOfDeath); + if (GameMain.GameSession != null && Screen.Selected == GameMain.GameScreen) + { + SteamAchievementManager.OnCharacterKilled(this, CauseOfDeath); + } KillProjSpecific(causeOfDeath, causeOfDeathAffliction, log); @@ -3023,13 +3201,13 @@ namespace Barotrauma public IEnumerable GetAttackContexts() { currentContexts.Clear(); - if (AnimController.CurrentAnimationParams.IsGroundedAnimation) + if (AnimController.InWater) { - currentContexts.Add(AttackContext.Ground); + currentContexts.Add(AttackContext.Water); } else { - currentContexts.Add(AttackContext.Water); + currentContexts.Add(AttackContext.Ground); } if (CurrentHull == null) { @@ -3131,5 +3309,15 @@ namespace Barotrauma } return targetPos; } + + public bool IsCaptain => HasJob("captain"); + public bool IsEngineer => HasJob("engineer"); + public bool IsMechanic => HasJob("mechanic"); + public bool IsMedic => HasJob("medicaldoctor"); + public bool IsOfficer => HasJob("securityofficer"); + public bool IsAsssitant => HasJob("assistant"); + public bool IsWatchman => HasJob("watchman"); + + public bool HasJob(string identifier) => Info?.Job?.Prefab.Identifier == identifier; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index e1b4fe019..1876804b7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -4,7 +4,7 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; @@ -997,6 +997,15 @@ namespace Barotrauma faceAttachments = null; } + /// + /// Reset order data so it doesn't carry into further rounds, as the AI is "recreated" always in between rounds anyway. + /// + public void ResetCurrentOrder() + { + CurrentOrder = null; + CurrentOrderOption = ""; + } + public void Remove() { Character = null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs index 93728e6a7..afbee5aaa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterPrefab.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Text; using System.Xml.Linq; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index d8f268ea5..9652a54ad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -19,7 +19,7 @@ namespace Barotrauma public virtual float Strength { get { return _strength; } - set { _strength = value; } + set { _strength = MathHelper.Clamp(value, 0.0f, Prefab.MaxStrength); } } [Serialize("", true), Editable] @@ -184,6 +184,8 @@ namespace Barotrauma { _strength += currentEffect.StrengthChange * deltaTime * (1f - characterHealth.GetResistance(Prefab.Identifier)); } + // Don't use the property, because its virtual and some afflictions like husk overload it for external use. + _strength = MathHelper.Clamp(_strength, 0.0f, Prefab.MaxStrength); foreach (StatusEffect statusEffect in currentEffect.StatusEffects) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index cc0509492..80cdaa08d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -97,13 +97,15 @@ namespace Barotrauma private void ApplyDamage(float deltaTime, bool applyForce) { + int limbCount = character.AnimController.Limbs.Count(l => !l.ignoreCollisions && !l.IsSevered); foreach (Limb limb in character.AnimController.Limbs) { + if (limb.IsSevered) { continue; } float random = Rand.Value(Rand.RandSync.Server); huskInfection.Clear(); - huskInfection.Add(AfflictionPrefab.InternalDamage.Instantiate(random * deltaTime / character.AnimController.Limbs.Length)); + huskInfection.Add(AfflictionPrefab.InternalDamage.Instantiate(random * 10 * deltaTime / limbCount)); character.LastDamageSource = null; - float force = applyForce ? random * 0.1f * limb.Mass : 0; + float force = applyForce ? random * 0.5f * limb.Mass : 0; character.DamageLimb(limb.WorldPosition, limb, huskInfection, 0, false, force); } } @@ -186,18 +188,20 @@ namespace Barotrauma } } - if (character.Inventory.Items.Length != husk.Inventory.Items.Length) + if (character.Inventory != null && husk.Inventory != null) { - string errorMsg = "Failed to move items from the source character's inventory into a husk's inventory (inventory sizes don't match)"; - DebugConsole.ThrowError(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("AfflictionHusk.CreateAIHusk:InventoryMismatch", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); - yield return CoroutineStatus.Success; - } - - for (int i = 0; i < character.Inventory.Items.Length && i < husk.Inventory.Items.Length; i++) - { - if (character.Inventory.Items[i] == null) continue; - husk.Inventory.TryPutItem(character.Inventory.Items[i], i, true, false, null); + if (character.Inventory.Items.Length != husk.Inventory.Items.Length) + { + string errorMsg = "Failed to move items from the source character's inventory into a husk's inventory (inventory sizes don't match)"; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("AfflictionHusk.CreateAIHusk:InventoryMismatch", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + yield return CoroutineStatus.Success; + } + for (int i = 0; i < character.Inventory.Items.Length && i < husk.Inventory.Items.Length; i++) + { + if (character.Inventory.Items[i] == null) continue; + husk.Inventory.TryPutItem(character.Inventory.Items[i], i, true, false, null); + } } husk.SetStun(5); @@ -255,20 +259,19 @@ namespace Barotrauma Limb attachLimb = null; if (matchingAffliction.AttachLimbId > -1) { - attachLimb = ragdoll.Limbs.FirstOrDefault(l => l.Params.ID == matchingAffliction.AttachLimbId); + attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Params.ID == matchingAffliction.AttachLimbId); } else if (matchingAffliction.AttachLimbName != null) { - attachLimb = ragdoll.Limbs.FirstOrDefault(l => l.Name == matchingAffliction.AttachLimbName); + attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Name == matchingAffliction.AttachLimbName); } else if (matchingAffliction.AttachLimbType != LimbType.None) { - attachLimb = ragdoll.Limbs.FirstOrDefault(l => l.type == matchingAffliction.AttachLimbType); + attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.type == matchingAffliction.AttachLimbType); } if (attachLimb == null) { - DebugConsole.Log("Attachment limb not defined in the affliction prefab or no matching limb could be found. Using the appendage definition as it is."); - attachLimb = ragdoll.Limbs.FirstOrDefault(l => l.Params.ID == jointParams.Limb1); + attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Params.ID == jointParams.Limb1); } if (attachLimb != null) { @@ -286,10 +289,6 @@ namespace Barotrauma ragdoll.AddJoint(jointParams); appendage.Add(huskAppendage); } - else - { - DebugConsole.ThrowError("Attachment limb not found!"); - } } } return appendage; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 5785866d5..7c90aa161 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -265,7 +265,8 @@ namespace Barotrauma public readonly Sprite Icon; public readonly Color[] IconColors; - private List effects = new List(); + private readonly List effects = new List(); + public IEnumerable Effects => effects; private readonly string typeName; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPsychosis.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPsychosis.cs index 7b3b9066f..a65e14cc8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPsychosis.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPsychosis.cs @@ -1,6 +1,6 @@ using Microsoft.Xna.Framework; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using Barotrauma.Extensions; using System.Xml.Linq; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Buffs/BuffDurationIncrease.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Buffs/BuffDurationIncrease.cs index 0b6fa2970..3b36e03a9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Buffs/BuffDurationIncrease.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Buffs/BuffDurationIncrease.cs @@ -1,4 +1,5 @@ using Microsoft.Xna.Framework; +using System; namespace Barotrauma { @@ -19,7 +20,7 @@ namespace Barotrauma { foreach (Affliction affliction in afflictions) { - if (!affliction.Prefab.IsBuff || affliction == this || affliction.MultiplierSource != this) continue; + if (!affliction.Prefab.IsBuff || affliction == this || affliction.MultiplierSource != this) { continue; } affliction.MultiplierSource = null; affliction.StrengthDiminishMultiplier = 1f; } @@ -28,9 +29,9 @@ namespace Barotrauma { foreach (Affliction affliction in afflictions) { - if (!affliction.Prefab.IsBuff || affliction == this || affliction.MultiplierSource == this) continue; + if (!affliction.Prefab.IsBuff || affliction == this) { continue; } float multiplier = GetDiminishMultiplier(); - if (affliction.StrengthDiminishMultiplier < multiplier) continue; + if (affliction.StrengthDiminishMultiplier < multiplier && affliction.MultiplierSource != this) { continue; } affliction.MultiplierSource = this; affliction.StrengthDiminishMultiplier = multiplier; @@ -40,14 +41,15 @@ namespace Barotrauma private float GetDiminishMultiplier() { - if (Strength < Prefab.ActivationThreshold) return 1.0f; + if (Strength < Prefab.ActivationThreshold) { return 1.0f; } AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength); - if (currentEffect == null) return 1.0f; + if (currentEffect == null) { return 1.0f; } - return MathHelper.Lerp( + float multiplier = MathHelper.Lerp( currentEffect.MinBuffMultiplier, currentEffect.MaxBuffMultiplier, (Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength)); + return 1.0f / Math.Max(multiplier, 0.001f); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index c411e2abb..b5e866b60 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -349,13 +349,16 @@ namespace Barotrauma /// Most monsters for example don't have separate healths for different limbs, essentially meaning that every affliction is applied to every limb. public float GetAfflictionStrength(string afflictionType, Limb limb, bool requireLimbSpecific) { - if (requireLimbSpecific && limbHealths.Count == 1) return 0.0f; + if (requireLimbSpecific && limbHealths.Count == 1) { return 0.0f; } float strength = 0.0f; foreach (Affliction affliction in limbHealths[limb.HealthIndex].Afflictions) { - if (affliction.Strength < affliction.Prefab.ActivationThreshold) continue; - if (affliction.Prefab.AfflictionType == afflictionType) strength += affliction.Strength; + if (affliction.Strength < affliction.Prefab.ActivationThreshold) { continue; } + if (affliction.Prefab.AfflictionType == afflictionType) + { + strength += affliction.Strength; + } } return strength; } @@ -365,17 +368,23 @@ namespace Barotrauma float strength = 0.0f; foreach (Affliction affliction in afflictions) { - if (affliction.Strength < affliction.Prefab.ActivationThreshold) continue; - if (affliction.Prefab.AfflictionType == afflictionType) strength += affliction.Strength; + if (affliction.Strength < affliction.Prefab.ActivationThreshold) { continue; } + if (affliction.Prefab.AfflictionType == afflictionType) + { + strength += affliction.Strength; + } } - if (!allowLimbAfflictions) return strength; + if (!allowLimbAfflictions) { return strength; } foreach (LimbHealth limbHealth in limbHealths) { foreach (Affliction affliction in limbHealth.Afflictions) { - if (affliction.Strength < affliction.Prefab.ActivationThreshold) continue; - if (affliction.Prefab.AfflictionType == afflictionType) strength += affliction.Strength; + if (affliction.Strength < affliction.Prefab.ActivationThreshold) { continue; } + if (affliction.Prefab.AfflictionType == afflictionType) + { + strength += affliction.Strength; + } } } @@ -419,11 +428,11 @@ namespace Barotrauma return resistance; } - private List matchingAfflictions = new List(); + private readonly List matchingAfflictions = new List(); public void ReduceAffliction(Limb targetLimb, string affliction, float amount) { matchingAfflictions.Clear(); - + matchingAfflictions.AddRange(afflictions); if (targetLimb != null) { matchingAfflictions.AddRange(limbHealths[targetLimb.HealthIndex].Afflictions); @@ -506,6 +515,27 @@ namespace Barotrauma if (Vitality <= MinVitality) { Kill(); } } + public float GetLimbDamage(Limb limb) + { + float damageStrength; + if (limb.IsSevered) + { + return 1; + } + else + { + // Instead of using the limbhealth count here, I think it's best to define the max vitality per limb roughly with a constant value. + // Therefore with e.g. 80 health, the max damage per limb would be 20. + // Having at least 20 damage on both legs would cause maximum limping. + float max = MaxVitality / 4; + float damage = GetAfflictionStrength("damage", limb, true); + float bleeding = GetAfflictionStrength("bleeding", limb, true); + float burn = GetAfflictionStrength("burn", limb, true); + damageStrength = Math.Min(damage + bleeding + burn, max); + return damageStrength / max; + } + } + public void RemoveAllAfflictions() { foreach (LimbHealth limbHealth in limbHealths) @@ -523,7 +553,7 @@ namespace Barotrauma private void AddLimbAffliction(Limb limb, Affliction newAffliction) { - if (!newAffliction.Prefab.LimbSpecific || limb == null) return; + if (!newAffliction.Prefab.LimbSpecific || limb == null) { return; } if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count) { DebugConsole.ThrowError("Limb health index out of bounds. Character\"" + Character.Name + @@ -535,8 +565,8 @@ namespace Barotrauma private void AddLimbAffliction(LimbHealth limbHealth, Affliction newAffliction) { - if (!DoesBleed && newAffliction is AfflictionBleeding) return; - if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) return; + if (!DoesBleed && newAffliction is AfflictionBleeding) { return; } + if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) { return; } foreach (Affliction affliction in limbHealth.Afflictions) { @@ -545,7 +575,10 @@ namespace Barotrauma affliction.Strength = Math.Min(affliction.Prefab.MaxStrength, affliction.Strength + (newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(affliction.Prefab.Identifier)))); affliction.Source = newAffliction.Source; CalculateVitality(); - if (Vitality <= MinVitality) Kill(); + if (Vitality <= MinVitality) + { + Kill(); + } return; } } @@ -560,7 +593,10 @@ namespace Barotrauma Character.HealthUpdateInterval = 0.0f; CalculateVitality(); - if (Vitality <= MinVitality) Kill(); + if (Vitality <= MinVitality) + { + Kill(); + } #if CLIENT selectedLimbIndex = -1; #endif @@ -894,10 +930,7 @@ namespace Barotrauma partial void RemoveProjSpecific(); - /// - /// Automatically filters out buffs. - /// - public static IEnumerable SortAfflictionsBySeverity(IEnumerable afflictions) => - afflictions.Where(a => !a.Prefab.IsBuff).OrderByDescending(a => a.DamagePerSecond).ThenByDescending(a => a.Strength); + public static IEnumerable SortAfflictionsBySeverity(IEnumerable afflictions, bool excludeBuffs = true) => + afflictions.Where(a => !excludeBuffs || !a.Prefab.IsBuff).OrderByDescending(a => a.DamagePerSecond).ThenByDescending(a => a.Strength); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs index 7439a6f30..bffa27eca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs @@ -19,6 +19,13 @@ namespace Barotrauma private set; } + [Serialize(1.0f, false), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = 1)] + public float ProbabilityMultiplier + { + get; + private set; + } + [Serialize("0.0,360", false), Editable] public Vector2 ArmorSector { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs index 68a0cf326..a257c66d3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs @@ -80,8 +80,8 @@ namespace Barotrauma public float GetSkillLevel(string skillIdentifier) { + if (string.IsNullOrWhiteSpace(skillIdentifier)) { return 0.0f; } skills.TryGetValue(skillIdentifier, out Skill skill); - return (skill == null) ? 0.0f : skill.Level; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs index 3b738e1b9..fbd323fc8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs @@ -64,7 +64,7 @@ namespace Barotrauma public readonly Dictionary> ItemIdentifiers = new Dictionary>(); public readonly Dictionary> ShowItemPreview = new Dictionary>(); public readonly List Skills = new List(); - public readonly List AutomaticOrders = new List(); + public readonly List AutonomousObjective = new List(); public readonly List AppropriateOrders = new List(); [Serialize("1,1,1,1", false)] @@ -198,7 +198,7 @@ namespace Barotrauma } break; case "autonomousobjectives": - subElement.Elements().ForEach(order => AutomaticOrders.Add(new AutonomousObjective(order))); + subElement.Elements().ForEach(order => AutonomousObjective.Add(new AutonomousObjective(order))); break; case "appropriateobjectives": case "appropriateorders": diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index b990d25d7..910a38ecb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -166,10 +166,20 @@ namespace Barotrauma if (isSevered) { ragdoll.SubtractMass(this); + if (type == LimbType.Head) + { + character.Kill(CauseOfDeathType.Unknown, null); + } + } + else + { + severedFadeOutTimer = 0.0f; } - if (!isSevered) severedFadeOutTimer = 0.0f; #if CLIENT - if (isSevered) damageOverlayStrength = 100.0f; + if (isSevered) + { + damageOverlayStrength = 100.0f; + } #endif } } @@ -366,12 +376,42 @@ namespace Barotrauma public string Name => Params.Name; + // Exposed for status effects + public bool IsDead => character.IsDead; + + public bool CanBeSeveredAlive + { + get + { + if (character.IsHumanoid) { return false; } + if (this == character.AnimController.MainLimb) { return false; } + if (character.AnimController.CanWalk) + { + switch (type) + { + case LimbType.LeftFoot: + case LimbType.RightFoot: + case LimbType.LeftLeg: + case LimbType.RightLeg: + case LimbType.LeftThigh: + case LimbType.RightThigh: + case LimbType.Legs: + case LimbType.Waist: + return false; + } + } + return true; + } + } + public Dictionary SerializableProperties { get; private set; } + private readonly List statusEffects = new List(); + public Limb(Ragdoll ragdoll, Character character, LimbParams limbParams) { this.ragdoll = ragdoll; @@ -434,6 +474,9 @@ namespace Barotrauma case "damagemodifier": DamageModifiers.Add(new DamageModifier(subElement, character.Name)); break; + case "statuseffect": + statusEffects.Add(StatusEffect.Load(subElement, Name)); + break; } } @@ -471,43 +514,60 @@ namespace Barotrauma return AddDamage(simPosition, afflictions, playSound); } + private readonly List appliedDamageModifiers = new List(); + private readonly List afflictionsCopy = new List(); public AttackResult AddDamage(Vector2 simPosition, IEnumerable afflictions, bool playSound) { - List appliedDamageModifiers = new List(); - //create a copy of the original affliction list to prevent modifying the afflictions of an Attack/StatusEffect etc - var afflictionsCopy = afflictions.Where(a => Rand.Range(0.0f, 1.0f) <= a.Probability).ToList(); - for (int i = 0; i < afflictionsCopy.Count; i++) + appliedDamageModifiers.Clear(); + afflictionsCopy.Clear(); + foreach (var affliction in afflictions) { + var newAffliction = affliction; + float random = Rand.Value(Rand.RandSync.Unsynced); + if (random > affliction.Probability) { continue; } + bool applyAffliction = true; foreach (DamageModifier damageModifier in DamageModifiers) { - if (!damageModifier.MatchesAffliction(afflictionsCopy[i])) continue; + if (!damageModifier.MatchesAffliction(affliction)) { continue; } + if (random > affliction.Probability * damageModifier.ProbabilityMultiplier) + { + applyAffliction = false; + continue; + } if (SectorHit(damageModifier.ArmorSectorInRadians, simPosition)) { - afflictionsCopy[i] = afflictionsCopy[i].CreateMultiplied(damageModifier.DamageMultiplier); + newAffliction = affliction.CreateMultiplied(damageModifier.DamageMultiplier); appliedDamageModifiers.Add(damageModifier); } } - foreach (WearableSprite wearable in wearingItems) { foreach (DamageModifier damageModifier in wearable.WearableComponent.DamageModifiers) { - if (!damageModifier.MatchesAffliction(afflictionsCopy[i])) continue; + if (!damageModifier.MatchesAffliction(affliction)) { continue; } + if (random > affliction.Probability * damageModifier.ProbabilityMultiplier) + { + applyAffliction = false; + continue; + } if (SectorHit(damageModifier.ArmorSectorInRadians, simPosition)) { - afflictionsCopy[i] = afflictionsCopy[i].CreateMultiplied(damageModifier.DamageMultiplier); + newAffliction = affliction.CreateMultiplied(damageModifier.DamageMultiplier); appliedDamageModifiers.Add(damageModifier); } } } + if (applyAffliction) + { + afflictionsCopy.Add(newAffliction); + } } - - AddDamageProjSpecific(simPosition, afflictionsCopy, playSound, appliedDamageModifiers); - - return new AttackResult(afflictionsCopy, this, appliedDamageModifiers); + var result = new AttackResult(afflictionsCopy, this, appliedDamageModifiers); + AddDamageProjSpecific(playSound, result); + return result; } - partial void AddDamageProjSpecific(Vector2 simPosition, List afflictions, bool playSound, List appliedDamageModifiers); + partial void AddDamageProjSpecific(bool playSound, AttackResult result); public bool SectorHit(Vector2 armorSector, Vector2 simPosition) { @@ -564,7 +624,8 @@ namespace Barotrauma public bool UpdateAttack(float deltaTime, Vector2 attackSimPos, IDamageable damageTarget, out AttackResult attackResult, float distance = -1, Limb targetLimb = null) { attackResult = default(AttackResult); - float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(SimPosition, attackSimPos)); + Vector2 simPos = ragdoll.SimplePhysicsEnabled ? character.SimPosition : SimPosition; + float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(simPos, attackSimPos)); bool wasRunning = attack.IsRunning; attack.UpdateAttackTimer(deltaTime); @@ -577,7 +638,7 @@ namespace Barotrauma case HitDetection.Distance: if (dist < attack.DamageRange) { - structureBody = Submarine.PickBody(SimPosition, attackSimPos, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel, allowInsideFixture: true); + structureBody = Submarine.PickBody(simPos, attackSimPos, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel, allowInsideFixture: true); if (damageTarget is Item i && i.GetComponent() != null) { // If the attack is aimed to an item and hits an item, it's successful. @@ -646,33 +707,19 @@ namespace Barotrauma if (wasHit) { - bool playSound = false; -#if CLIENT - playSound = LastAttackSoundTime < Timing.TotalTime - SoundInterval; - if (playSound) + if (character == Character.Controlled || GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) { - LastAttackSoundTime = SoundInterval; + ExecuteAttack(damageTarget, targetLimb, out attackResult); } +#if SERVER + GameMain.NetworkMember.CreateEntityEvent(character, new object[] + { + NetEntityEvent.Type.ExecuteAttack, + this, + (damageTarget as Entity)?.ID ?? Entity.NullEntityID, + damageTarget is Character && targetLimb != null ? Array.IndexOf(((Character)damageTarget).AnimController.Limbs, targetLimb) : 0 + }); #endif - if (damageTarget is Character targetCharacter && targetLimb != null) - { - attackResult = attack.DoDamageToLimb(character, targetLimb, WorldPosition, 1.0f, playSound); - } - else - { - attackResult = attack.DoDamage(character, damageTarget, WorldPosition, 1.0f, playSound); - } - if (structureBody != null && attack.StickChance > Rand.Range(0.0f, 1.0f, Rand.RandSync.Server)) - { - // TODO: use the hit pos? - var localFront = body.GetLocalFront(Params.GetSpriteOrientation()); - var from = body.FarseerBody.GetWorldPoint(localFront); - var to = from; - var drawPos = body.DrawPosition; - StickTo(structureBody, from, to); - } - attack.ResetAttackTimer(); - attack.SetCoolDown(); } Vector2 diff = attackSimPos - SimPosition; @@ -685,6 +732,7 @@ namespace Barotrauma { if (limbIndex < 0 || limbIndex >= character.AnimController.Limbs.Length) { continue; } Limb limb = character.AnimController.Limbs[limbIndex]; + if (limb.IsSevered) { continue; } diff = attackSimPos - limb.SimPosition; if (diff == Vector2.Zero) { continue; } limb.body.ApplyTorque(limb.Mass * character.AnimController.Dir * attack.Torque * limb.Params.AttackForceMultiplier); @@ -705,11 +753,49 @@ namespace Barotrauma if (!attack.IsRunning) { // Set the main collider where the body lands after the attack - character.AnimController.Collider.SetTransform(character.AnimController.MainLimb.body.SimPosition, rotation: 0); + character.AnimController.Collider.SetTransform(character.AnimController.MainLimb.body.SimPosition, rotation: character.AnimController.Collider.Rotation); } return wasHit; } + public void ExecuteAttack(IDamageable damageTarget, Limb targetLimb, out AttackResult attackResult) + { + bool playSound = false; +#if CLIENT + playSound = LastAttackSoundTime < Timing.TotalTime - SoundInterval; + if (playSound) + { + LastAttackSoundTime = SoundInterval; + } +#endif + if (damageTarget is Character targetCharacter && targetLimb != null) + { + attackResult = attack.DoDamageToLimb(character, targetLimb, WorldPosition, 1.0f, playSound); + } + else + { + if (damageTarget is Item targetItem && !targetItem.Prefab.DamagedByMonsters) + { + attackResult = new AttackResult(); + } + else + { + attackResult = attack.DoDamage(character, damageTarget, WorldPosition, 1.0f, playSound); + } + } + /*if (structureBody != null && attack.StickChance > Rand.Range(0.0f, 1.0f, Rand.RandSync.Server)) + { + // TODO: use the hit pos? + var localFront = body.GetLocalFront(Params.GetSpriteOrientation()); + var from = body.FarseerBody.GetWorldPoint(localFront); + var to = from; + var drawPos = body.DrawPosition; + StickTo(structureBody, from, to); + }*/ + attack.ResetAttackTimer(); + attack.SetCoolDown(); + } + private WeldJoint attachJoint; private WeldJoint colliderJoint; public bool IsStuck => attachJoint != null; @@ -769,6 +855,30 @@ namespace Barotrauma } } + private readonly List targets = new List(); + public void ApplyStatusEffects(ActionType actionType, float deltaTime) + { + foreach (StatusEffect statusEffect in statusEffects) + { + if (statusEffect.type != actionType) { continue; } + if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) || + statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) + { + targets.Clear(); + statusEffect.GetNearbyTargets(WorldPosition, targets); + statusEffect.Apply(ActionType.OnActive, deltaTime, character, targets); + } + else + { + if (statusEffect.HasTargetType(StatusEffect.TargetType.Character)) + { + statusEffect.Apply(actionType, deltaTime, character, character, WorldPosition); + } + statusEffect.Apply(actionType, deltaTime, character, this, WorldPosition); + } + } + } + public void Remove() { body?.Remove(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs index 9f97ec0be..52d3ba651 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs @@ -1,7 +1,7 @@ using Microsoft.Xna.Framework; using System.Collections.Generic; using System.Globalization; -using System.IO; +using Barotrauma.IO; using System; using System.Linq; using System.Xml.Linq; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs index 0d9f8a24e..275336876 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/FishAnimations.cs @@ -72,6 +72,12 @@ namespace Barotrauma [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.")] public bool Flip { get; set; } + [Serialize(1f, true, description: "Reduces continuous flipping when the character abruptly changes direction."), Editable] + public float FlipCooldown { get; set; } + + [Serialize(0.5f, true, description: "How much it takes before the character flips. The timer starts when the character starts to move in the different direction."), Editable] + public float FlipDelay { get; set; } + [Serialize(10.0f, true, description: "How much force is used to move the head to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)] public float HeadMoveForce { get; set; } @@ -146,9 +152,18 @@ namespace Barotrauma [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.")] public bool Flip { get; set; } + [Serialize(1f, true, description: "Reduces continuous flipping when the character abruptly changes direction."), Editable] + public float FlipCooldown { get; set; } + + [Serialize(0.5f, true, description: "How much it takes before the character flips. The timer starts when the character starts to move in the different direction."), Editable] + public float FlipDelay { get; set; } + [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; } + [Editable, Serialize(true, true, description: "Disabling this will make mirroring instantaneous.")] + public bool MirrorLerp { get; set; } + [Serialize(5f, true), Editable] public float WaveAmplitude { get; set; } @@ -205,7 +220,6 @@ namespace Barotrauma interface IFishAnimation { - bool Flip { get; set; } string FootAngles { get; set; } Dictionary FootAnglesInRadians { get; set; } float TailAngle { get; set; } @@ -214,5 +228,8 @@ namespace Barotrauma float TorsoTorque { get; set; } float TailTorque { get; set; } float FootTorque { get; set; } + bool Flip { get; set; } + float FlipCooldown { get; set; } + float FlipDelay { get; set; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 52d131b72..98d2eff2e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -25,10 +25,10 @@ namespace Barotrauma [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; } - [Serialize(false, true), Editable] + [Serialize(false, true), Editable(ReadOnly = true)] public bool Humanoid { get; private set; } - [Serialize(false, true), Editable] + [Serialize(false, true), Editable(ReadOnly = true)] public bool HasInfo { get; private set; } [Serialize(false, true), Editable] @@ -43,24 +43,36 @@ namespace Barotrauma [Serialize(false, true, description: "Can the creature live without water or does it die on dry land?"), Editable] public bool NeedsWater { get; set; } - [Serialize(false, true), Editable] + [Serialize(false, false), Editable] public bool CanSpeak { get; set; } - [Serialize(100f, true, description: "How much noise the character makes when moving?"), Editable(minValue: 0f, maxValue: 1000f)] + [Serialize(100f, true, description: "How much noise the character makes when moving?"), Editable(minValue: 0f, maxValue: 10000f)] public float Noise { get; set; } - [Serialize(100f, true, description: "How visible the character is?"), Editable(minValue: 0f, maxValue: 1000f)] + [Serialize(100f, true, description: "How visible the character is?"), Editable(minValue: 0f, maxValue: 10000f)] public float Visibility { get; set; } [Serialize("blood", true), Editable] public string BloodDecal { get; private set; } + [Serialize("blooddrop", true), Editable] + public string BleedParticleAir { get; private set; } + + [Serialize("waterblood", true), Editable] + public string BleedParticleWater { get; private set; } + [Serialize(10f, true, description: "How effectively/easily the character eats other characters. Affects the forces, the amount of particles, and the time required before the target is eaten away"), Editable(MinValueFloat = 1, MaxValueFloat = 1000, ValueStep = 1)] public float EatingSpeed { get; set; } [Serialize(1f, true, "Decreases the intensive path finding call frequency. Set to a lower value for insignificant creatures to improve performance."), Editable(minValue: 0f, maxValue: 1f)] public float PathFinderPriority { get; set; } + [Serialize(false, true), Editable] + public bool HideInSonar { get; set; } + + [Serialize(0f, true), Editable] + public float SonarDisruption { get; set; } + public readonly string File; public readonly List SubParams = new List(); @@ -465,8 +477,11 @@ namespace Barotrauma [Serialize(true, true, description: "Enforce aggressive behavior if the creature is spawned as a target of a monster mission."), Editable()] public bool EnforceAggressiveBehaviorForMissions { get; private set; } - [Serialize(false, true, description: "Should the character target or ignore walls when it's inside the submarine. Doesn't have any effect if no target priority for walls is defined."), Editable()] - public bool TargetInnerWalls { get; private set; } + [Serialize(true, true, description: "Should the character target or ignore walls when it's outside the submarine. Doesn't have any effect if no target priority for walls is defined."), Editable()] + public bool TargetOuterWalls { get; private set; } + + [Serialize(false, true, description: "If enabled, the character chooses randomly from the available attacks. The priority is used as a weight for weighted random."), Editable()] + public bool RandomAttack { get; private set; } // TODO: latchonto, swarming diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs index 66bf5f3c3..1a7fa97f7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/EditableParams.cs @@ -1,8 +1,12 @@ -using System.IO; -using System.Collections.Generic; -using System.Xml; +using System.Collections.Generic; using System.Xml.Linq; using Microsoft.Xna.Framework; +#if DEBUG +using System.IO; +using System.Xml; +#else +using Barotrauma.IO; +#endif namespace Barotrauma { @@ -75,7 +79,7 @@ namespace Barotrauma Folder = Path.GetDirectoryName(FullPath); } - public virtual bool Save(string fileNameWithoutExtension = null, XmlWriterSettings settings = null) + public virtual bool Save(string fileNameWithoutExtension = null, System.Xml.XmlWriterSettings settings = null) { if (!Directory.Exists(Folder)) { @@ -85,7 +89,7 @@ namespace Barotrauma Serialize(); if (settings == null) { - settings = new XmlWriterSettings + settings = new System.Xml.XmlWriterSettings { Indent = true, OmitXmlDeclaration = true, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs index 0101cff43..c45cb5faa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; using System.Xml.Linq; using System.Linq; -using System.IO; +using Barotrauma.IO; using System.Xml; using Barotrauma.Extensions; #if CLIENT @@ -470,6 +470,12 @@ namespace Barotrauma [Serialize(true, true), Editable] public bool CanBeSevered { get; set; } + [Serialize(1f, true, description:"Modifies the severance probability (defined per item/attack) when the character is alive. Currently only affects limbs of type None, Shield, or Tail on non-humanoid ragdolls. Also note that if CanBeSevered is false, this property doesn't have any effect."), Editable(MinValueFloat = 0, MaxValueFloat = 10, ValueStep = 0.1f, DecimalCount = 2)] + public float SeveranceProbabilityModifier { get; set; } + + [Serialize("gore", true), Editable] + public string BreakSound { get; set; } + [Serialize(true, true), Editable] public bool LimitEnabled { get; set; } @@ -605,7 +611,11 @@ namespace Barotrauma [Serialize(1f, true), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = 10)] public float AttackForceMultiplier { get; set; } + [Serialize(1f, true, description:"How much damage must be done by the attack in order to be able to cut off the limb. Note that it's evaluated after the damage modifiers."), Editable(DecimalCount = 0, MinValueFloat = 0, MaxValueFloat = 1000)] + public float MinSeveranceDamage { get; set; } + // Non-editable -> + // TODO: make read-only [Serialize(0, true)] public int HealthIndex { get; set; } @@ -813,6 +823,15 @@ namespace Barotrauma [Serialize("", true), Editable()] public string Texture { get; set; } + [Serialize("1.0,1.0,1.0,1.0", true), Editable()] + public Color Color { get; set; } + + [Serialize("1.0,1.0,1.0,1.0", true, description: "Target color when the character is dead."), Editable()] + public Color DeadColor { get; set; } + + [Serialize(0f, true, "How long it takes to fade into the dead color? 0 = Not applied."), Editable(DecimalCount = 1, MinValueFloat = 0, MaxValueFloat = 10)] + public float DeadColorTime { get; set; } + public override string Name => "Sprite"; public SpriteParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) { } @@ -927,7 +946,7 @@ namespace Barotrauma { public override string Name => "Light Texture"; - [Serialize("", true), Editable] + [Serialize("Content/Lights/pointlight_bright.png", true), Editable] public string Texture { get; private set; } [Serialize("0.5, 0.5", true), Editable(DecimalCount = 2)] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs index abe6aab58..433da2d10 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs @@ -128,7 +128,7 @@ namespace Barotrauma if (Current == null) { - DebugConsole.NewMessage("Now skill settings found in the selected content packages. Using default values."); + DebugConsole.NewMessage("No skill settings found in the selected content packages. Using default values."); Current = new SkillSettings(null); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs index a9fe770d2..4be3f7eed 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Security.Cryptography; using System.Xml.Linq; @@ -53,7 +53,7 @@ namespace Barotrauma //these types of files are included in the MD5 hash calculation, //meaning that the players must have the exact same files to play together - private static HashSet multiplayerIncompatibleContent = new HashSet + public static HashSet MultiplayerIncompatibleContent { get; private set; } = new HashSet { ContentType.Jobs, ContentType.Item, @@ -67,6 +67,7 @@ namespace Barotrauma ContentType.RuinConfig, ContentType.Outpost, ContentType.Wreck, + ContentType.WreckAIConfig, ContentType.Afflictions, ContentType.Orders, ContentType.Corpses @@ -81,6 +82,7 @@ namespace Barotrauma ContentType.Structure, ContentType.Outpost, ContentType.Wreck, + ContentType.WreckAIConfig, ContentType.Text, ContentType.Executable, ContentType.ServerExecutable, @@ -159,7 +161,7 @@ namespace Barotrauma public bool HasMultiplayerIncompatibleContent { - get { return Files.Any(f => multiplayerIncompatibleContent.Contains(f.Type)); } + get { return Files.Any(f => MultiplayerIncompatibleContent.Contains(f.Type)); } } private ContentPackage() @@ -412,7 +414,42 @@ namespace Barotrauma doc.Root.Add(new XElement(file.Type.ToString(), new XAttribute("file", file.Path.CleanUpPathCrossPlatform()))); } - doc.Save(filePath); + doc.SaveSafe(filePath); + + var packagesToDeselect = List.Where(p => p.Path.CleanUpPath() == Path.CleanUpPath()).ToList(); + bool reselectPackage = false; + + if (packagesToDeselect.Any()) + { + foreach (var p in packagesToDeselect) + { + if (GameMain.Config.SelectedContentPackages.Contains(p)) + { + reselectPackage = true; + if (p.CorePackage) + { + GameMain.Config.SelectCorePackage(List.Find(cpp => cpp.CorePackage && !packagesToDeselect.Contains(cpp))); + } + else + { + GameMain.Config.DeselectContentPackage(p); + } + } + List.Remove(p); + } + List.Add(this); + if (reselectPackage) + { + if (CorePackage) + { + GameMain.Config.SelectCorePackage(this); + } + else + { + GameMain.Config.SelectContentPackage(this); + } + } + } } public void CalculateHash(bool logging = false) @@ -426,7 +463,7 @@ namespace Barotrauma foreach (ContentFile file in Files) { - if (!multiplayerIncompatibleContent.Contains(file.Type)) { continue; } + if (!MultiplayerIncompatibleContent.Contains(file.Type)) { continue; } try { @@ -537,7 +574,7 @@ namespace Barotrauma while (true) { - string temp = System.IO.Path.GetDirectoryName(path); + string temp = Barotrauma.IO.Path.GetDirectoryName(path); if (string.IsNullOrEmpty(temp)) { break; } path = temp; } @@ -578,7 +615,7 @@ namespace Barotrauma } } - string[] files = Directory.GetFiles(folder, "*.xml"); + IEnumerable files = Directory.GetFiles(folder, "*.xml"); List.Clear(); @@ -587,12 +624,12 @@ namespace Barotrauma List.Add(new ContentPackage(filePath)); } - string[] modDirectories = Directory.GetDirectories("Mods"); + IEnumerable modDirectories = Directory.GetDirectories("Mods"); foreach (string modDirectory in modDirectories) { - if (System.IO.Path.GetFileName(modDirectory.TrimEnd(System.IO.Path.DirectorySeparatorChar)) == "ExampleMod") { continue; } - string modFilePath = System.IO.Path.Combine(modDirectory, Steam.SteamManager.MetadataFileName); - string copyingFilePath = System.IO.Path.Combine(modDirectory, Steam.SteamManager.CopyIndicatorFileName); + if (Barotrauma.IO.Path.GetFileName(modDirectory.TrimEnd(Barotrauma.IO.Path.DirectorySeparatorChar)) == "ExampleMod") { continue; } + string modFilePath = Barotrauma.IO.Path.Combine(modDirectory, Steam.SteamManager.MetadataFileName); + string copyingFilePath = Barotrauma.IO.Path.Combine(modDirectory, Steam.SteamManager.CopyIndicatorFileName); if (File.Exists(copyingFilePath)) { //this mod didn't clean up its copying file; assume it's corrupted and delete it diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 88728b6fc..ac85be2a8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -8,7 +8,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Text; @@ -678,7 +678,7 @@ namespace Barotrauma NewMessage(Hull.EditFire ? "Fire spawning on" : "Fire spawning off", Color.White); }, isCheat: true)); - commands.Add(new Command("explosion", "explosion [range] [force] [damage] [structuredamage] [emp strength]: Creates an explosion at the position of the cursor.", null, isCheat: true)); + commands.Add(new Command("explosion", "explosion [range] [force] [damage] [structuredamage] [item damage] [emp strength]: Creates an explosion at the position of the cursor.", null, isCheat: true)); commands.Add(new Command("showseed|showlevelseed", "showseed: Show the seed of the current level.", (string[] args) => { @@ -692,7 +692,7 @@ namespace Barotrauma } },null)); - commands.Add(new Command("teleportsub", "teleportsub [start/end]: Teleport the submarine to the start or end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.", (string[] args) => + commands.Add(new Command("teleportsub", "teleportsub [start/end/cursor]: Teleport the submarine to the position of the cursor, or the start or end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.", (string[] args) => { if (Submarine.MainSub == null || Level.Loaded == null) return; @@ -975,6 +975,22 @@ namespace Barotrauma } })); + commands.Add(new Command("money", "", args => + { + if (args.Length == 0) { return; } + if (GameMain.GameSession.GameMode is CampaignMode campaign) + { + if (int.TryParse(args[0], out int money)) + { + campaign.Money += money; + } + else + { + ThrowError($"\"{args[0]}\" is not a valid numeric value."); + } + } + }, isCheat: true)); + commands.Add(new Command("difficulty|leveldifficulty", "difficulty [0-100]: Change the level difficulty setting in the server lobby.", null)); commands.Add(new Command("autoitemplacerdebug|outfitdebug", "autoitemplacerdebug: Toggle automatic item placer debug info on/off. The automatically placed items are listed in the debug console at the start of a round.", (string[] args) => diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index cdcb77796..3d70417af 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Barotrauma +namespace Barotrauma { public enum TransitionMode { @@ -13,4 +9,17 @@ namespace Barotrauma EaseOut, Exponential } + + public enum ActionType + { + Always, OnPicked, OnUse, OnSecondaryUse, + OnWearing, OnContaining, OnContained, OnNotContained, + OnActive, OnFailure, OnBroken, + OnFire, InWater, NotInWater, + OnImpact, + OnEating, + OnDeath = OnBroken, + OnDamaged, + OnSevered + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index f8e552ad9..28b55c962 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using FarseerPhysics; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; @@ -9,6 +10,8 @@ namespace Barotrauma { const float IntensityUpdateInterval = 5.0f; + const float CalculateDistanceTraveledInterval = 5.0f; + private Level level; private readonly List preloadedSprites = new List(); @@ -30,6 +33,11 @@ namespace Barotrauma private float intensityUpdateTimer; + private PathFinder pathFinder; + private float totalPathLength; + private float calculateDistanceTraveledTimer; + private float distanceTraveled; + private float avgCrewHealth, avgHullIntegrity, floodingAmount, fireAmount, enemyDanger; private float roundDuration; @@ -72,6 +80,10 @@ namespace Barotrauma pendingEventSets.Clear(); selectedEvents.Clear(); + pathFinder = new PathFinder(WayPoint.WayPointList, indoorsSteering: false); + var steeringPath = pathFinder.FindPath(ConvertUnits.ToSimUnits(Level.Loaded.StartPosition), ConvertUnits.ToSimUnits(Level.Loaded.EndPosition)); + totalPathLength = steeringPath.TotalLength; + this.level = level; SelectSettings(); @@ -137,7 +149,44 @@ namespace Barotrauma public void PreloadContent(IEnumerable contentFiles) { - foreach (ContentFile file in contentFiles) + var filesToPreload = new List(contentFiles); + foreach (Submarine sub in Submarine.Loaded) + { + if (sub.WreckAI == null) { continue; } + + if (!string.IsNullOrEmpty(sub.WreckAI.Config.DefensiveAgent)) + { + var prefab = CharacterPrefab.FindBySpeciesName(sub.WreckAI.Config.DefensiveAgent); + if (prefab != null && !filesToPreload.Any(f => f.Path == prefab.FilePath)) + { + filesToPreload.Add(new ContentFile(prefab.FilePath, ContentType.Character)); + } + } + foreach (Item item in Item.ItemList) + { + if (item.Submarine != sub) { continue; } + foreach (Items.Components.ItemComponent component in item.Components) + { + if (component.statusEffectLists == null) { continue; } + foreach (var statusEffectList in component.statusEffectLists.Values) + { + foreach (StatusEffect statusEffect in statusEffectList) + { + foreach (var spawnInfo in statusEffect.SpawnCharacters) + { + var prefab = CharacterPrefab.FindBySpeciesName(spawnInfo.SpeciesName); + if (prefab != null && !filesToPreload.Any(f => f.Path == prefab.FilePath)) + { + filesToPreload.Add(new ContentFile(prefab.FilePath, ContentType.Character)); + } + } + } + } + } + } + } + + foreach (ContentFile file in filesToPreload) { switch (file.Type) { @@ -225,7 +274,7 @@ namespace Barotrauma } else if (eventSet.PerWreck) { - applyCount = Submarine.Loaded.Count(s => s.Info.IsWreck && (s.ThalamusAI == null || !s.ThalamusAI.IsAlive)); + applyCount = Submarine.Loaded.Count(s => s.Info.IsWreck && (s.WreckAI == null || !s.WreckAI.IsAlive)); } for (int i = 0; i < applyCount; i++) { @@ -299,12 +348,9 @@ namespace Barotrauma private bool CanStartEventSet(ScriptedEventSet eventSet) { - float distFromStart = Vector2.Distance(Submarine.MainSub.WorldPosition, level.StartPosition); - float distFromEnd = Vector2.Distance(Submarine.MainSub.WorldPosition, level.EndPosition); - - float distanceTraveled = MathHelper.Clamp( - (Submarine.MainSub.WorldPosition.X - level.StartPosition.X) / (level.EndPosition.X - level.StartPosition.X), - 0.0f, 1.0f); + ISpatialEntity refEntity = GetRefEntity(); + float distFromStart = Vector2.Distance(refEntity.WorldPosition, level.StartPosition); + float distFromEnd = Vector2.Distance(refEntity.WorldPosition, level.EndPosition); //don't create new events if within 50 meters of the start/end of the level if (!eventSet.AllowAtStart) @@ -367,6 +413,13 @@ namespace Barotrauma } } + calculateDistanceTraveledTimer -= deltaTime; + if (calculateDistanceTraveledTimer <= 0.0f) + { + distanceTraveled = CalculateDistanceTraveled(); + calculateDistanceTraveledTimer = CalculateDistanceTraveledInterval; + } + eventThreshold += settings.EventThresholdIncrease * deltaTime; if (eventCoolDown > 0.0f) { @@ -514,5 +567,62 @@ namespace Barotrauma currentIntensity = MathHelper.Max(0.0025f * IntensityUpdateInterval, targetIntensity); } } + + private float CalculateDistanceTraveled() + { + var refEntity = GetRefEntity(); + Vector2 target = ConvertUnits.ToSimUnits(Level.Loaded.EndPosition); + var steeringPath = pathFinder.FindPath(ConvertUnits.ToSimUnits(refEntity.WorldPosition), target); + if (steeringPath.Unreachable || float.IsPositiveInfinity(totalPathLength)) + { + //use horizontal position in the level as a fallback if a path can't be found + return MathHelper.Clamp((refEntity.WorldPosition.X - level.StartPosition.X) / (level.EndPosition.X - level.StartPosition.X), 0.0f, 1.0f); + } + else + { + return MathHelper.Clamp(1.0f - steeringPath.TotalLength / totalPathLength, 0.0f, 1.0f); + } + } + + + /// + /// Get the entity that should be used in determining how far the player has progressed in the level. + /// = The submarine or player character that has progressed the furthest. + /// + private ISpatialEntity GetRefEntity() + { + ISpatialEntity refEntity = Submarine.MainSub; +#if CLIENT + if (Character.Controlled != null) + { + if (Character.Controlled.Submarine != null && + Character.Controlled.Submarine.Info.Type == SubmarineInfo.SubmarineType.Player) + { + refEntity = Character.Controlled.Submarine; + } + else + { + refEntity = Character.Controlled; + } + } +#else + foreach (Barotrauma.Networking.Client client in GameMain.Server.ConnectedClients) + { + if (client.Character == null) { continue; } + //only take the players inside a player sub into account. + //Otherwise the system could be abused by for example making a respawned player wait + //close to the destination outpost + if (client.Character.Submarine != null && + client.Character.Submarine.Info.Type == SubmarineInfo.SubmarineType.Player) + { + if (client.Character.Submarine.WorldPosition.X > refEntity.WorldPosition.X) + { + refEntity = client.Character.Submarine; + } + } + } +#endif + return refEntity; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManagerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManagerSettings.cs index d6923a024..1f425f4ff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManagerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManagerSettings.cs @@ -24,8 +24,9 @@ namespace Barotrauma public readonly float MinLevelDifficulty = 0.0f; public readonly float MaxLevelDifficulty = 100.0f; - static EventManagerSettings() + public static void Init() { + List.Clear(); foreach (ContentFile file in GameMain.Instance.GetFilesOfType(ContentType.EventManagerSettings)) { Load(file); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs index 20c2dafd9..222cad5c0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CombatMission.cs @@ -108,23 +108,6 @@ namespace Barotrauma subs[1].SetPosition(subs[1].FindSpawnPos(Level.Loaded.EndPosition)); subs[1].FlipX(); - //prevent wifi components from communicating between subs - List wifiComponents = new List(); - foreach (Item item in Item.ItemList) - { - wifiComponents.AddRange(item.GetComponents()); - } - foreach (WifiComponent wifiComponent in wifiComponents) - { - for (int i = 0; i < 2; i++) - { - if (wifiComponent.Item.Submarine == subs[i] || subs[i].ConnectedDockingPorts.ContainsKey(wifiComponent.Item.Submarine)) - { - wifiComponent.TeamID = subs[i].TeamID; - } - } - } - crews = new List[] { new List(), new List() }; foreach (Submarine submarine in Submarine.Loaded) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs index 88c43fabb..7bc3c98f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MonsterMission.cs @@ -70,7 +70,7 @@ namespace Barotrauma monsterFiles.Add(new Tuple(monster, new Point(min, max))); } description = description.Replace("[monster]", - TextManager.Get("character." + System.IO.Path.GetFileNameWithoutExtension(monsterFileName))); + TextManager.Get("character." + Barotrauma.IO.Path.GetFileNameWithoutExtension(monsterFileName))); } public override void Start(Level level) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index 70c347779..2c3333212 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -1,8 +1,10 @@ -using FarseerPhysics; +using Barotrauma.Extensions; +using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; +using System.Xml.Linq; namespace Barotrauma { @@ -17,11 +19,14 @@ namespace Barotrauma private readonly string containerTag; private readonly string existingItemTag; - - private bool usedExistingItem; - + private readonly bool showMessageWhenPickedUp; + /// + /// Status effects executed on the target item when the mission starts. A random effect is chosen from each child list. + /// + private readonly List> statusEffects = new List>(); + public override IEnumerable SonarPositions { get @@ -71,6 +76,29 @@ namespace Barotrauma { spawnPositionType = Level.PositionType.Cave | Level.PositionType.Ruin; } + + foreach (XElement element in prefab.ConfigElement.Elements()) + { + switch (element.Name.ToString().ToLowerInvariant()) + { + case "statuseffect": + { + var newEffect = StatusEffect.Load(element, parentDebugName: prefab.Name); + if (newEffect == null) { continue; } + statusEffects.Add(new List { newEffect }); + break; + } + case "chooserandom": + statusEffects.Add(new List()); + foreach (XElement subElement in element.Elements()) + { + var newEffect = StatusEffect.Load(subElement, parentDebugName: prefab.Name); + if (newEffect == null) { continue; } + statusEffects.Last().Add(newEffect); + } + break; + } + } } public override void Start(Level level) @@ -103,7 +131,9 @@ namespace Barotrauma if (Submarine.RectContains(worldBorders, it.WorldPosition)) { item = it; +#if SERVER usedExistingItem = true; +#endif break; } } @@ -118,6 +148,18 @@ namespace Barotrauma item.FindHull(); } + for (int i = 0; i < statusEffects.Count; i++) + { + List effectList = statusEffects[i]; + if (effectList.Count == 0) { continue; } + int effectIndex = Rand.Int(effectList.Count); + var selectedEffect = effectList[effectIndex]; + item.ApplyStatusEffect(selectedEffect, selectedEffect.type, deltaTime: 1.0f, worldPosition: item.Position); +#if SERVER + executedEffectIndices.Add(new Pair(i, effectIndex)); +#endif + } + //try to find a container and place the item inside it if (!string.IsNullOrEmpty(containerTag) && item.ParentInventory == null) { @@ -147,15 +189,23 @@ namespace Barotrauma public override void Update(float deltaTime) { + if (item == null) + { +#if DEBUG + DebugConsole.ThrowError("Error in salvage mission " + Prefab.Identifier + " (item was null)"); +#endif + return; + } + if (IsClient) { - if (item.ParentInventory != null) { item.body.FarseerBody.BodyType = BodyType.Dynamic; } + if (item.ParentInventory != null && item.body != null) { item.body.FarseerBody.BodyType = BodyType.Dynamic; } return; } switch (State) { case 0: - if (item.ParentInventory != null) { item.body.FarseerBody.BodyType = BodyType.Dynamic; } + if (item.ParentInventory != null && item.body != null) { item.body.FarseerBody.BodyType = BodyType.Dynamic; } if (showMessageWhenPickedUp) { if (!(item.ParentInventory?.Owner is Character)) { return; } @@ -175,7 +225,10 @@ namespace Barotrauma 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.CurrentHull.Submarine.AtStartPosition) || item.Removed) + { + return; + } item?.Remove(); item = null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index 8be908783..ff9a21312 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -118,13 +118,13 @@ namespace Barotrauma private List GetAvailableSpawnPositions() { - var availablePositions = Level.Loaded.PositionsOfInterest.FindAll(p => spawnPosType.HasFlag(p.PositionType) && !Level.Loaded.UsedPositions.Contains(p)); + var availablePositions = Level.Loaded.PositionsOfInterest.FindAll(p => spawnPosType.HasFlag(p.PositionType)); var removals = new List(); foreach (var position in availablePositions) { if (position.Submarine != null) { - if (position.Submarine.ThalamusAI != null && position.Submarine.ThalamusAI.IsAlive) + if (position.Submarine.WreckAI != null && position.Submarine.WreckAI.IsAlive) { removals.Add(position); } @@ -169,10 +169,6 @@ namespace Barotrauma if (Rand.Value(Rand.RandSync.Server) > prefab.SpawnProbability) { removedPositions.Add(position); - if (prefab.AllowOnlyOnce) - { - Level.Loaded.UsedPositions.Add(position); - } } } removedPositions.ForEach(p => availablePositions.Remove(p)); @@ -237,17 +233,15 @@ namespace Barotrauma spawnPos = chosenPosition.Position.ToVector2(); if (chosenPosition.Submarine != null || chosenPosition.Ruin != null) { - var spawnPoint = WayPoint.GetRandom(SpawnType.Enemy, sub: chosenPosition.Submarine, useSyncedRand: false); - if (spawnPoint != null) + var spawnPoint = WayPoint.GetRandom(SpawnType.Enemy, sub: chosenPosition.Submarine, ruin: chosenPosition.Ruin, useSyncedRand: false); + if (spawnPoint != null) { - spawnPos = spawnPoint.WorldPosition; + System.Diagnostics.Debug.Assert(spawnPoint.Submarine == chosenPosition.Submarine); + System.Diagnostics.Debug.Assert(spawnPoint.ParentRuin == chosenPosition.Ruin); + spawnPos = spawnPoint.WorldPosition; } } spawnPending = true; - if (prefab.AllowOnlyOnce) - { - Level.Loaded.UsedPositions.Add(chosenPosition); - } } } @@ -276,11 +270,14 @@ namespace Barotrauma if (spawnPending) { //wait until there are no submarines at the spawnpos - foreach (Submarine submarine in Submarine.Loaded) + if (spawnPosType == Level.PositionType.MainPath) { - if (submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; } - float minDist = GetMinDistanceToSub(submarine); - if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist) { return; } + foreach (Submarine submarine in Submarine.Loaded) + { + if (submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; } + float minDist = GetMinDistanceToSub(submarine); + if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist) { return; } + } } //if spawning in a ruin/cave, wait for someone to be close to it to spawning diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEventPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEventPrefab.cs index a6dc76b1e..877988d39 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEventPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEventPrefab.cs @@ -11,7 +11,6 @@ namespace Barotrauma public readonly Type EventType; public readonly string MusicType; public readonly float SpawnProbability; - public readonly bool AllowOnlyOnce; public float Commonness; public ScriptedEventPrefab(XElement element) @@ -34,7 +33,6 @@ namespace Barotrauma } Commonness = element.GetAttributeFloat("commonness", 1.0f); SpawnProbability = Math.Clamp(element.GetAttributeFloat("spawnprobability", 1.0f), 0, 1); - AllowOnlyOnce = element.GetAttributeBool("allowonlyonce", false); } public ScriptedEvent CreateInstance() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs index b2dfb78c1..ecc1c2d72 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs @@ -32,13 +32,22 @@ namespace Barotrauma.Extensions public static T GetRandom(this IEnumerable source, Func predicate, Rand.RandSync randSync = Rand.RandSync.Unsynced) { + if (predicate == null) { return GetRandom(source, randSync); } return source.Where(predicate).GetRandom(randSync); } public static T GetRandom(this IEnumerable source, Rand.RandSync randSync = Rand.RandSync.Unsynced) { - int count = source.Count(); - return count == 0 ? default(T) : source.ElementAt(Rand.Range(0, count, randSync)); + if (source is IList list) + { + int count = list.Count; + return count == 0 ? default : list[Rand.Range(0, count, randSync)]; + } + else + { + int count = source.Count(); + return count == 0 ? default : source.ElementAt(Rand.Range(0, count, randSync)); + } } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/FrameCounter.cs b/Barotrauma/BarotraumaShared/SharedSource/FrameCounter.cs index 647d4c6d8..0b4d5c016 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/FrameCounter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/FrameCounter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; namespace Barotrauma @@ -42,7 +43,7 @@ namespace Barotrauma public float GetAverageElapsedMillisecs(string identifier) { if (!avgTicksPerFrame.ContainsKey(identifier)) return 0.0f; - return avgTicksPerFrame[identifier] / (float)TimeSpan.TicksPerMillisecond; + return avgTicksPerFrame[identifier] * 1000.0f / (float)Stopwatch.Frequency; } public bool Update(double deltaTime) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameAnalyticsManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameAnalyticsManager.cs index b634bf35a..3975c1cd3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameAnalyticsManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameAnalyticsManager.cs @@ -2,7 +2,7 @@ using System; using System.Text; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Reflection; using System.Security.Cryptography; diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index d60200a7a..2cf23ecdb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -160,7 +160,7 @@ namespace Barotrauma bool success = false; if (Rand.Value() > validContainer.Value.SpawnProbability) { return false; } // Don't add dangerously reactive materials in thalamus wrecks - if (validContainer.Key.Item.Submarine.ThalamusAI != null && itemPrefab.Tags.Contains("explodesinwater")) + if (validContainer.Key.Item.Submarine.WreckAI != null && itemPrefab.Tags.Contains("explodesinwater")) { return false; } @@ -174,6 +174,10 @@ namespace Barotrauma } var item = new Item(itemPrefab, validContainer.Key.Item.Position, validContainer.Key.Item.Submarine); + foreach (WifiComponent wifiComponent in item.GetComponents()) + { + wifiComponent.TeamID = validContainer.Key.Item.Submarine.TeamID; + } spawnedItems.Add(item); #if SERVER Entity.Spawner.CreateNetworkEvent(item, remove: false); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index ad7a1b428..02d14dbb9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -1,4 +1,5 @@ using Barotrauma.Items.Components; +using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -20,8 +21,9 @@ namespace Barotrauma class CargoManager { - private readonly List purchasedItems; + public const int MaxQuantity = 100; + private readonly List purchasedItems; private readonly CampaignMode campaign; public Action OnItemsChanged; @@ -115,9 +117,25 @@ namespace Barotrauma ItemPrefab containerPrefab = null; foreach (PurchasedItem pi in itemsToSpawn) { + float floorPos = cargoRoom.Rect.Y - cargoRoom.Rect.Height; + Vector2 position = new Vector2( Rand.Range(cargoRoom.Rect.X + 20, cargoRoom.Rect.Right - 20), - cargoRoom.Rect.Y - cargoRoom.Rect.Height + pi.ItemPrefab.Size.Y / 2); + floorPos); + + //check where the actual floor structure is in case the bottom of the hull extends below it + if (Submarine.PickBody( + ConvertUnits.ToSimUnits(new Vector2(position.X, cargoRoom.Rect.Y - cargoRoom.Rect.Height / 2)), + ConvertUnits.ToSimUnits(position), + collisionCategory: Physics.CollisionWall) != null) + { + float floorStructurePos = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition.Y); + if (floorStructurePos > floorPos) + { + floorPos = floorStructurePos; + } + } + position.Y = floorPos + pi.ItemPrefab.Size.Y / 2; ItemContainer itemContainer = null; if (!string.IsNullOrEmpty(pi.ItemPrefab.CargoContainerIdentifier)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index 39fd4452c..3561f8ce9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -4,7 +4,7 @@ using System; using System.Linq; using System.Xml.Linq; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; namespace Barotrauma { @@ -113,6 +113,7 @@ namespace Barotrauma { if (c.Character?.Info != null && !c.Character.IsDead) { + c.Character.ResetCurrentOrder(); c.CharacterInfo = c.Character.Info; characterData.Add(new CharacterCampaignData(c)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 47490a8d7..c90e1ed40 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using Barotrauma.IO; +using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -9,7 +10,7 @@ namespace Barotrauma { partial class GameSession { - public enum InfoFrameTab { Crew, Mission, MyCharacter, ManagePlayers }; + public enum InfoFrameTab { Crew, Mission, MyCharacter, Traitor }; public readonly EventManager EventManager; @@ -231,7 +232,7 @@ namespace Barotrauma if (port.Item.WorldPosition.Y < Submarine.WorldPosition.Y) { continue; } float dist = Vector2.DistanceSquared(port.Item.WorldPosition, level.StartOutpost.WorldPosition); - if (myPort == null || dist < closestDistance || (port.MainDockingPort && !myPort.MainDockingPort)) + if ((myPort == null || dist < closestDistance || port.MainDockingPort) && !(myPort?.MainDockingPort ?? false)) { myPort = port; closestDistance = dist; @@ -351,6 +352,8 @@ namespace Barotrauma OnClicked = (GUIButton button, object obj) => { GUIMessageBox.MessageBoxes.Remove(summaryFrame); return true; } }; } + + TabMenu.OnRoundEnded(); #endif EventManager?.EndRound(); @@ -461,7 +464,7 @@ namespace Barotrauma try { - doc.Save(filePath); + doc.SaveSafe(filePath); } catch (Exception e) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs index cf1ddf4a9..58d0c9bc0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs @@ -2,8 +2,7 @@ using System.Xml.Linq; using System.Collections.Generic; using Microsoft.Xna.Framework; -using System.Xml; -using System.IO; +using Barotrauma.IO; using Barotrauma.Extensions; #if CLIENT using Microsoft.Xna.Framework.Input; @@ -37,6 +36,8 @@ namespace Barotrauma public bool VSyncEnabled { get; set; } + public bool TextureCompressionEnabled { get; set; } + public bool EnableSplashScreen { get; set; } public int ParticleLimit { get; set; } @@ -66,6 +67,7 @@ namespace Barotrauma #if CLIENT private KeyOrMouse[] keyMapping; + private KeyOrMouse[] inventoryKeyMapping; #endif private WindowMode windowMode; @@ -91,6 +93,7 @@ namespace Barotrauma set { /*do nothing*/ } } #endif + public bool UseDualModeSockets { get; set; } = true; public bool AutoUpdateWorkshopItems; @@ -117,6 +120,19 @@ namespace Barotrauma set { jobPreferences = value; } } + public bool AreJobPreferencesEqual(List> compareTo) + { + if (jobPreferences == null || compareTo == null) return false; + if (jobPreferences.Count != compareTo.Count) return false; + + for (int i = 0; i < jobPreferences.Count; i++) + { + if (jobPreferences[i].First != compareTo[i].First || jobPreferences[i].Second != compareTo[i].Second) return false; + } + + return true; + } + public int CharacterHeadIndex { get; set; } public int CharacterHairIndex { get; set; } public int CharacterBeardIndex { get; set; } @@ -193,7 +209,7 @@ namespace Barotrauma { musicVolume = MathHelper.Clamp(value, 0.0f, 1.0f); #if CLIENT - GameMain.SoundManager?.SetCategoryGainMultiplier("music", musicVolume, 0); + GameMain.SoundManager?.SetCategoryGainMultiplier("music", musicVolume * 0.7f, 0); #endif } } @@ -203,13 +219,20 @@ namespace Barotrauma get { return voiceChatVolume; } set { - voiceChatVolume = MathHelper.Clamp(value, 0.0f, 1.0f); + voiceChatVolume = MathHelper.Clamp(value, 0.0f, 2.0f); #if CLIENT - GameMain.SoundManager?.SetCategoryGainMultiplier("voip", voiceChatVolume, 0); + GameMain.SoundManager?.SetCategoryGainMultiplier("voip", Math.Min(voiceChatVolume, 1.0f), 0); #endif } } + + public int VoiceChatCutoffPrevention + { + get; + set; + } + public const float MaxMicrophoneVolume = 10.0f; public float MicrophoneVolume { @@ -246,7 +269,7 @@ namespace Barotrauma public bool TextManagerDebugModeEnabled { get; set; } #endif - private FileSystemWatcher modsFolderWatcher; + private System.IO.FileSystemWatcher modsFolderWatcher; private int ContentFileLoadOrder(ContentFile a) { @@ -280,58 +303,11 @@ namespace Barotrauma !otherCorePackage.Files.Any(f2 => Path.GetFullPath(f1.Path).CleanUpPath() == Path.GetFullPath(f2.Path).CleanUpPath())).ToList(); - bool shouldRefreshSubs = false; - bool shouldRefreshFabricationRecipes = false; - bool shouldRefreshSoundPlayer = false; - bool shouldRefreshRuinGenerationParams = false; - bool shouldRefreshScriptedEventSets = false; - bool shouldRefreshMissionPrefabs = false; - bool shouldRefreshLevelObjectPrefabs = false; - bool shouldRefreshLocationTypes = false; - bool shouldRefreshMapGenerationParams = false; - bool shouldRefreshLevelGenerationParams = false; - bool shouldRefreshAfflictions = false; + DisableContentPackageItems(filesToRemove.OrderBy(ContentFileLoadOrder)); - DisableContentPackageItems(filesToRemove.OrderBy(ContentFileLoadOrder), - ref shouldRefreshSubs, - ref shouldRefreshFabricationRecipes, - ref shouldRefreshSoundPlayer, - ref shouldRefreshRuinGenerationParams, - ref shouldRefreshScriptedEventSets, - ref shouldRefreshMissionPrefabs, - ref shouldRefreshLevelObjectPrefabs, - ref shouldRefreshLocationTypes, - ref shouldRefreshMapGenerationParams, - ref shouldRefreshLevelGenerationParams, - ref shouldRefreshAfflictions); + EnableContentPackageItems(filesToAdd.OrderBy(ContentFileLoadOrder)); - EnableContentPackageItems(filesToAdd.OrderBy(ContentFileLoadOrder), - ref shouldRefreshSubs, - ref shouldRefreshFabricationRecipes, - ref shouldRefreshSoundPlayer, - ref shouldRefreshRuinGenerationParams, - ref shouldRefreshScriptedEventSets, - ref shouldRefreshMissionPrefabs, - ref shouldRefreshLevelObjectPrefabs, - ref shouldRefreshLocationTypes, - ref shouldRefreshMapGenerationParams, - ref shouldRefreshLevelGenerationParams, - ref shouldRefreshAfflictions); - - if (shouldRefreshAfflictions) { AfflictionPrefab.LoadAll(GameMain.Instance.GetFilesOfType(ContentType.Afflictions)); } - if (shouldRefreshSubs) { SubmarineInfo.RefreshSavedSubs(); } - if (shouldRefreshFabricationRecipes) { ItemPrefab.InitFabricationRecipes(); } - if (shouldRefreshRuinGenerationParams) { RuinGeneration.RuinGenerationParams.ClearAll(); } - if (shouldRefreshScriptedEventSets) { ScriptedEventSet.LoadPrefabs(); } - if (shouldRefreshMissionPrefabs) { MissionPrefab.Init(); } - if (shouldRefreshLevelObjectPrefabs) { LevelObjectPrefab.LoadAll(); } - if (shouldRefreshLocationTypes) { LocationType.Init(); } - if (shouldRefreshMapGenerationParams) { MapGenerationParams.Init(); } - if (shouldRefreshLevelGenerationParams) { LevelGenerationParams.LoadPresets(); } - -#if CLIENT - if (shouldRefreshSoundPlayer) { SoundPlayer.Init().ForEach(_ => { return; }); } -#endif + RefreshContentPackageItems(filesToAdd.Concat(filesToRemove)); } public void SelectContentPackage(ContentPackage contentPackage) @@ -341,45 +317,9 @@ namespace Barotrauma SelectedContentPackages.Add(contentPackage); ContentPackage.SortContentPackages(); - bool shouldRefreshSubs = false; - bool shouldRefreshFabricationRecipes = false; - bool shouldRefreshSoundPlayer = false; - bool shouldRefreshRuinGenerationParams = false; - bool shouldRefreshScriptedEventSets = false; - bool shouldRefreshMissionPrefabs = false; - bool shouldRefreshLevelObjectPrefabs = false; - bool shouldRefreshLocationTypes = false; - bool shouldRefreshMapGenerationParams = false; - bool shouldRefreshLevelGenerationParams = false; - bool shouldRefreshAfflictions = false; + EnableContentPackageItems(contentPackage.Files.OrderBy(ContentFileLoadOrder)); - EnableContentPackageItems(contentPackage.Files.OrderBy(ContentFileLoadOrder), - ref shouldRefreshSubs, - ref shouldRefreshFabricationRecipes, - ref shouldRefreshSoundPlayer, - ref shouldRefreshRuinGenerationParams, - ref shouldRefreshScriptedEventSets, - ref shouldRefreshMissionPrefabs, - ref shouldRefreshLevelObjectPrefabs, - ref shouldRefreshLocationTypes, - ref shouldRefreshMapGenerationParams, - ref shouldRefreshLevelGenerationParams, - ref shouldRefreshAfflictions); - - if (shouldRefreshAfflictions) { AfflictionPrefab.LoadAll(GameMain.Instance.GetFilesOfType(ContentType.Afflictions)); } - if (shouldRefreshSubs) { SubmarineInfo.RefreshSavedSubs(); } - if (shouldRefreshFabricationRecipes) { ItemPrefab.InitFabricationRecipes(); } - if (shouldRefreshRuinGenerationParams) { RuinGeneration.RuinGenerationParams.ClearAll(); } - if (shouldRefreshScriptedEventSets) { ScriptedEventSet.LoadPrefabs(); } - if (shouldRefreshMissionPrefabs) { MissionPrefab.Init(); } - if (shouldRefreshLevelObjectPrefabs) { LevelObjectPrefab.LoadAll(); } - if (shouldRefreshLocationTypes) { LocationType.Init(); } - if (shouldRefreshMapGenerationParams) { MapGenerationParams.Init(); } - if (shouldRefreshLevelGenerationParams) { LevelGenerationParams.LoadPresets(); } - -#if CLIENT - if (shouldRefreshSoundPlayer) { SoundPlayer.Init().ForEach(_ => { return; }); } -#endif + RefreshContentPackageItems(contentPackage.Files); } } @@ -390,61 +330,14 @@ namespace Barotrauma SelectedContentPackages.Remove(contentPackage); ContentPackage.SortContentPackages(); - bool shouldRefreshSubs = false; - bool shouldRefreshFabricationRecipes = false; - bool shouldRefreshSoundPlayer = false; - bool shouldRefreshRuinGenerationParams = false; - bool shouldRefreshScriptedEventSets = false; - bool shouldRefreshMissionPrefabs = false; - bool shouldRefreshLevelObjectPrefabs = false; - bool shouldRefreshLocationTypes = false; - bool shouldRefreshMapGenerationParams = false; - bool shouldRefreshLevelGenerationParams = false; - bool shouldRefreshAfflictions = false; + DisableContentPackageItems(contentPackage.Files.OrderBy(ContentFileLoadOrder)); - DisableContentPackageItems(contentPackage.Files.OrderBy(ContentFileLoadOrder), - ref shouldRefreshSubs, - ref shouldRefreshFabricationRecipes, - ref shouldRefreshSoundPlayer, - ref shouldRefreshRuinGenerationParams, - ref shouldRefreshScriptedEventSets, - ref shouldRefreshMissionPrefabs, - ref shouldRefreshLevelObjectPrefabs, - ref shouldRefreshLocationTypes, - ref shouldRefreshMapGenerationParams, - ref shouldRefreshLevelGenerationParams, - ref shouldRefreshAfflictions); - - if (shouldRefreshAfflictions) { AfflictionPrefab.LoadAll(GameMain.Instance.GetFilesOfType(ContentType.Afflictions)); } - if (shouldRefreshSubs) { SubmarineInfo.RefreshSavedSubs(); } - if (shouldRefreshFabricationRecipes) { ItemPrefab.InitFabricationRecipes(); } - if (shouldRefreshRuinGenerationParams) { RuinGeneration.RuinGenerationParams.ClearAll(); } - if (shouldRefreshScriptedEventSets) { ScriptedEventSet.LoadPrefabs(); } - if (shouldRefreshMissionPrefabs) { MissionPrefab.Init(); } - if (shouldRefreshLevelObjectPrefabs) { LevelObjectPrefab.LoadAll(); } - if (shouldRefreshLocationTypes) { LocationType.Init(); } - if (shouldRefreshMapGenerationParams) { MapGenerationParams.Init(); } - if (shouldRefreshLevelGenerationParams) { LevelGenerationParams.LoadPresets(); } - -#if CLIENT - if (shouldRefreshSoundPlayer) { SoundPlayer.Init().ForEach(_ => { return; }); } -#endif + RefreshContentPackageItems(contentPackage.Files); } } - private void EnableContentPackageItems(IOrderedEnumerable files, - ref bool shouldRefreshSubs, - ref bool shouldRefreshFabricationRecipes, - ref bool shouldRefreshSoundPlayer, - ref bool shouldRefreshRuinGenerationParams, - ref bool shouldRefreshScriptedEventSets, - ref bool shouldRefreshMissionPrefabs, - ref bool shouldRefreshLevelObjectPrefabs, - ref bool shouldRefreshLocationTypes, - ref bool shouldRefreshMapGenerationParams, - ref bool shouldRefreshLevelGenerationParams, - ref bool shouldRefreshAfflictions) + private void EnableContentPackageItems(IOrderedEnumerable files) { foreach (ContentFile file in files) { @@ -453,6 +346,9 @@ namespace Barotrauma case ContentType.Character: CharacterPrefab.LoadFromFile(file); break; + case ContentType.Corpses: + CorpsePrefab.LoadFromFile(file); + break; case ContentType.NPCConversations: NPCConversation.LoadFromFile(file); break; @@ -461,7 +357,6 @@ namespace Barotrauma break; case ContentType.Item: ItemPrefab.LoadFromFile(file); - shouldRefreshFabricationRecipes = true; break; case ContentType.ItemAssembly: new ItemAssemblyPrefab(file.Path); @@ -469,40 +364,10 @@ namespace Barotrauma case ContentType.Structure: StructurePrefab.LoadFromFile(file); break; - case ContentType.Submarine: - shouldRefreshSubs = true; - break; case ContentType.Text: TextManager.LoadTextPack(file.Path); break; - case ContentType.Afflictions: - shouldRefreshAfflictions = true; - break; - case ContentType.RuinConfig: - shouldRefreshRuinGenerationParams = true; - break; - case ContentType.RandomEvents: - shouldRefreshScriptedEventSets = true; - break; - case ContentType.Missions: - shouldRefreshMissionPrefabs = true; - break; - case ContentType.LevelObjectPrefabs: - shouldRefreshLevelObjectPrefabs = true; - break; - case ContentType.LocationTypes: - shouldRefreshLocationTypes = true; - break; - case ContentType.MapGenerationParameters: - shouldRefreshMapGenerationParams = true; - break; - case ContentType.LevelGenerationParameters: - shouldRefreshLevelGenerationParams = true; - break; #if CLIENT - case ContentType.Sounds: - shouldRefreshSoundPlayer = true; - break; case ContentType.Particles: GameMain.ParticleManager?.LoadPrefabsFromFile(file); break; @@ -516,18 +381,7 @@ namespace Barotrauma } } - private void DisableContentPackageItems(IOrderedEnumerable files, - ref bool shouldRefreshSubs, - ref bool shouldRefreshFabricationRecipes, - ref bool shouldRefreshSoundPlayer, - ref bool shouldRefreshRuinGenerationParams, - ref bool shouldRefreshScriptedEventSets, - ref bool shouldRefreshMissionPrefabs, - ref bool shouldRefreshLevelObjectPrefabs, - ref bool shouldRefreshLocationTypes, - ref bool shouldRefreshMapGenerationParams, - ref bool shouldRefreshLevelGenerationParams, - ref bool shouldRefreshAfflictions) + private void DisableContentPackageItems(IOrderedEnumerable files) { foreach (ContentFile file in files) { @@ -536,6 +390,9 @@ namespace Barotrauma case ContentType.Character: CharacterPrefab.RemoveByFile(file.Path); break; + case ContentType.Corpses: + CorpsePrefab.RemoveByFile(file.Path); + break; case ContentType.NPCConversations: NPCConversation.RemoveByFile(file.Path); break; @@ -544,7 +401,6 @@ namespace Barotrauma break; case ContentType.Item: ItemPrefab.RemoveByFile(file.Path); - shouldRefreshFabricationRecipes = true; break; case ContentType.ItemAssembly: ItemAssemblyPrefab.Remove(file.Path); @@ -552,40 +408,10 @@ namespace Barotrauma case ContentType.Structure: StructurePrefab.RemoveByFile(file.Path); break; - case ContentType.Submarine: - shouldRefreshSubs = true; - break; case ContentType.Text: TextManager.RemoveTextPack(file.Path); break; - case ContentType.Afflictions: - shouldRefreshAfflictions = true; - break; - case ContentType.RuinConfig: - shouldRefreshRuinGenerationParams = true; - break; - case ContentType.RandomEvents: - shouldRefreshScriptedEventSets = true; - break; - case ContentType.Missions: - shouldRefreshMissionPrefabs = true; - break; - case ContentType.LevelObjectPrefabs: - shouldRefreshLevelObjectPrefabs = true; - break; - case ContentType.LocationTypes: - shouldRefreshLocationTypes = true; - break; - case ContentType.MapGenerationParameters: - shouldRefreshMapGenerationParams = true; - break; - case ContentType.LevelGenerationParameters: - shouldRefreshLevelGenerationParams = true; - break; #if CLIENT - case ContentType.Sounds: - shouldRefreshSoundPlayer = true; - break; case ContentType.Particles: GameMain.ParticleManager?.RemovePrefabsByFile(file.Path); break; @@ -599,39 +425,74 @@ namespace Barotrauma } } + private void RefreshContentPackageItems(IEnumerable files) + { + if (files.Any(f => f.Type == ContentType.Afflictions)) { AfflictionPrefab.LoadAll(GameMain.Instance.GetFilesOfType(ContentType.Afflictions)); } + if (files.Any(f => f.Type == ContentType.Submarine)) { SubmarineInfo.RefreshSavedSubs(); } + if (files.Any(f => f.Type == ContentType.Item)) { ItemPrefab.InitFabricationRecipes(); } + if (files.Any(f => f.Type == ContentType.RuinConfig)) { RuinGeneration.RuinGenerationParams.ClearAll(); } + if (files.Any(f => f.Type == ContentType.RandomEvents)) { ScriptedEventSet.LoadPrefabs(); } + if (files.Any(f => f.Type == ContentType.Missions)) { MissionPrefab.Init(); } + if (files.Any(f => f.Type == ContentType.LevelObjectPrefabs)) { LevelObjectPrefab.LoadAll(); } + if (files.Any(f => f.Type == ContentType.LocationTypes)) { LocationType.Init(); } + if (files.Any(f => f.Type == ContentType.MapGenerationParameters)) { MapGenerationParams.Init(); } + if (files.Any(f => f.Type == ContentType.LevelGenerationParameters)) { LevelGenerationParams.LoadPresets(); } + if (files.Any(f => f.Type == ContentType.TraitorMissions)) { TraitorMissionPrefab.Init(); } + if (files.Any(f => f.Type == ContentType.Orders)) { Order.Init(); } + if (files.Any(f => f.Type == ContentType.EventManagerSettings)) { EventManagerSettings.Init(); } + if (files.Any(f => f.Type == ContentType.WreckAIConfig)) { WreckAIConfig.LoadAll(); } + if (files.Any(f => f.Type == ContentType.SkillSettings)) { SkillSettings.Load(GameMain.Instance.GetFilesOfType(ContentType.SkillSettings)); } + +#if CLIENT + if (files.Any(f => f.Type == ContentType.Tutorials)) { Tutorial.Init(); } + if (files.Any(f => f.Type == ContentType.Sounds)) { SoundPlayer.Init().ForEach(_ => { return; }); } +#endif + } + + private readonly static ContentType[] hotswappableContentTypes = new ContentType[] + { + ContentType.Character, + ContentType.Corpses, + ContentType.NPCConversations, + ContentType.Jobs, + ContentType.Orders, + ContentType.EventManagerSettings, + ContentType.Item, + ContentType.ItemAssembly, + ContentType.Structure, + ContentType.Submarine, + ContentType.Text, + ContentType.Afflictions, + ContentType.RuinConfig, + ContentType.RandomEvents, + ContentType.Missions, + ContentType.LevelObjectPrefabs, + ContentType.LocationTypes, + ContentType.MapGenerationParameters, + ContentType.LevelGenerationParameters, + ContentType.Sounds, + ContentType.Particles, + ContentType.Decals, + ContentType.Outpost, + ContentType.Wreck, + ContentType.WreckAIConfig, + ContentType.BackgroundCreaturePrefabs, + ContentType.ServerExecutable, + ContentType.TraitorMissions, + ContentType.Tutorials, + ContentType.SkillSettings, + ContentType.None + }; + private void UpdateContentPackageDirtyFlag(ContentFile file) { - switch (file.Type) + if (!hotswappableContentTypes.Contains(file.Type)) { - case ContentType.Character: - case ContentType.NPCConversations: - case ContentType.Jobs: - case ContentType.Item: - case ContentType.ItemAssembly: - case ContentType.Structure: - case ContentType.Submarine: - case ContentType.Text: - case ContentType.Afflictions: - case ContentType.RuinConfig: - case ContentType.RandomEvents: - case ContentType.Missions: - case ContentType.LevelObjectPrefabs: - case ContentType.LocationTypes: - case ContentType.MapGenerationParameters: - case ContentType.LevelGenerationParameters: - case ContentType.Sounds: - case ContentType.Particles: - case ContentType.Decals: - case ContentType.Outpost: - case ContentType.Wreck: - case ContentType.BackgroundCreaturePrefabs: - case ContentType.ServerExecutable: - case ContentType.None: - break; //do nothing here if the content type is supported - default: + if (ContentPackage.MultiplayerIncompatibleContent.Contains(file.Type)) + { ContentPackageSelectionDirty = true; - ContentPackageSelectionDirtyNotification = true; - break; + } + ContentPackageSelectionDirtyNotification = true; } } @@ -661,6 +522,11 @@ namespace Barotrauma LocationType.Init(); MapGenerationParams.Init(); LevelGenerationParams.LoadPresets(); + TraitorMissionPrefab.Init(); + Order.Init(); + EventManagerSettings.Init(); + WreckAIConfig.LoadAll(); + SkillSettings.Load(GameMain.Instance.GetFilesOfType(ContentType.SkillSettings)); #if CLIENT GameMain.DecalManager.Prefabs.SortAll(); @@ -733,6 +599,10 @@ namespace Barotrauma public bool ShowLanguageSelectionPrompt { get; set; } private bool showTutorialSkipWarning = true; + + public static bool EnableSubmarineAutoSave { get; set; } + public static Color SubEditorBackgroundColor { get; set; } + public bool ShowTutorialSkipWarning { get { return showTutorialSkipWarning && CompletedTutorialNames.Count == 0; } @@ -755,21 +625,21 @@ namespace Barotrauma LoadPlayerConfig(); - modsFolderWatcher = new FileSystemWatcher("Mods"); + modsFolderWatcher = new System.IO.FileSystemWatcher("Mods"); modsFolderWatcher.Filter = "*"; - modsFolderWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName; + modsFolderWatcher.NotifyFilter = System.IO.NotifyFilters.LastWrite | System.IO.NotifyFilters.FileName | System.IO.NotifyFilters.DirectoryName; modsFolderWatcher.Created += OnModFolderUpdate; modsFolderWatcher.Deleted += OnModFolderUpdate; modsFolderWatcher.Renamed += OnModFolderUpdate; modsFolderWatcher.EnableRaisingEvents = true; } - private void OnModFolderUpdate(object sender, FileSystemEventArgs e) + private void OnModFolderUpdate(object sender, System.IO.FileSystemEventArgs e) { if (SuppressModFolderWatcher || (GameMain.NetworkMember?.IsClient ?? false)) { return; } switch (e.ChangeType) { - case WatcherChangeTypes.Created: + case System.IO.WatcherChangeTypes.Created: { string cpPath = Path.GetFullPath(Path.Combine(e.FullPath, Steam.SteamManager.MetadataFileName)).CleanUpPath(); if (File.Exists(cpPath) && !ContentPackage.List.Any(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath)) @@ -779,7 +649,7 @@ namespace Barotrauma } } break; - case WatcherChangeTypes.Deleted: + case System.IO.WatcherChangeTypes.Deleted: { string cpPath = Path.GetFullPath(Path.Combine(e.FullPath, Steam.SteamManager.MetadataFileName)).CleanUpPath(); var toRemove = ContentPackage.List.Where(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath).ToList(); @@ -802,9 +672,9 @@ namespace Barotrauma } } break; - case WatcherChangeTypes.Renamed: + case System.IO.WatcherChangeTypes.Renamed: { - RenamedEventArgs renameArgs = e as RenamedEventArgs; + System.IO.RenamedEventArgs renameArgs = e as System.IO.RenamedEventArgs; string cpPath = Path.GetFullPath(Path.Combine(renameArgs.OldFullPath, Steam.SteamManager.MetadataFileName)).CleanUpPath(); var toRemove = ContentPackage.List.Where(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath).ToList(); @@ -884,8 +754,11 @@ namespace Barotrauma new XAttribute("soundvolume", soundVolume), new XAttribute("microphonevolume", microphoneVolume), new XAttribute("voicechatvolume", voiceChatVolume), + new XAttribute("voicechatcutoffprevention", VoiceChatCutoffPrevention), new XAttribute("verboselogging", VerboseLogging), new XAttribute("savedebugconsolelogs", SaveDebugConsoleLogs), + new XAttribute("submarineautosave", EnableSubmarineAutoSave), + new XAttribute("subeditorbackground", XMLExtensions.ColorToString(SubEditorBackgroundColor)), new XAttribute("enablesplashscreen", EnableSplashScreen), new XAttribute("usesteammatchmaking", UseSteamMatchmaking), new XAttribute("quickstartsub", QuickStartSubmarineName), @@ -952,13 +825,29 @@ namespace Barotrauma doc.Root.Add(keyMappingElement); for (int i = 0; i < keyMapping.Length; i++) { - if (keyMapping[i].MouseButton == MouseButton.None) + KeyOrMouse bind = keyMapping[i]; + if (bind.MouseButton == MouseButton.None) { - keyMappingElement.Add(new XAttribute(((InputType)i).ToString(), keyMapping[i].Key)); + keyMappingElement.Add(new XAttribute(((InputType)i).ToString(), bind.Key)); } else { - keyMappingElement.Add(new XAttribute(((InputType)i).ToString(), keyMapping[i].MouseButton)); + keyMappingElement.Add(new XAttribute(((InputType)i).ToString(), bind.MouseButton)); + } + } + + var inventoryKeyMappingElement = new XElement("inventorykeymapping"); + doc.Root.Add(inventoryKeyMappingElement); + for (int i = 0; i < inventoryKeyMapping.Length; i++) + { + KeyOrMouse bind = inventoryKeyMapping[i]; + if (bind.MouseButton == MouseButton.None) + { + inventoryKeyMappingElement.Add(new XAttribute($"slot{i}", bind.Key)); + } + else + { + inventoryKeyMappingElement.Add(new XAttribute($"slot{i}", bind.MouseButton)); } } #endif @@ -986,7 +875,7 @@ namespace Barotrauma new XAttribute("faceattachmentindex", CharacterFaceAttachmentIndex)); doc.Root.Add(playerElement); - XmlWriterSettings settings = new XmlWriterSettings + System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { Indent = true, OmitXmlDeclaration = true, @@ -1082,7 +971,7 @@ namespace Barotrauma SelectedContentPackages.Clear(); foreach (string path in contentPackagePaths) { - var matchingContentPackage = ContentPackage.List.Find(cp => System.IO.Path.GetFullPath(cp.Path).CleanUpPath() == path.CleanUpPath()); + var matchingContentPackage = ContentPackage.List.Find(cp => Barotrauma.IO.Path.GetFullPath(cp.Path).CleanUpPath() == path.CleanUpPath()); if (matchingContentPackage == null) { @@ -1196,6 +1085,8 @@ namespace Barotrauma new XAttribute("soundvolume", soundVolume), new XAttribute("verboselogging", VerboseLogging), new XAttribute("savedebugconsolelogs", SaveDebugConsoleLogs), + new XAttribute("submarineautosave", EnableSubmarineAutoSave), + new XAttribute("subeditorbackground", XMLExtensions.ColorToString(SubEditorBackgroundColor)), new XAttribute("enablesplashscreen", EnableSplashScreen), new XAttribute("usesteammatchmaking", UseSteamMatchmaking), new XAttribute("quickstartsub", QuickStartSubmarineName), @@ -1210,7 +1101,8 @@ namespace Barotrauma new XAttribute("editordisclaimershown", EditorDisclaimerShown), new XAttribute("tutorialskipwarning", ShowTutorialSkipWarning), new XAttribute("corpsedespawndelay", CorpseDespawnDelay), - new XAttribute("corpsespersubdespawnthreshold", CorpsesPerSubDespawnThreshold) + new XAttribute("corpsespersubdespawnthreshold", CorpsesPerSubDespawnThreshold), + new XAttribute("usedualmodesockets", UseDualModeSockets) #if DEBUG , new XAttribute("automaticquickstartenabled", AutomaticQuickStartEnabled) , new XAttribute("textmanagerdebugmodeenabled", TextManagerDebugModeEnabled) @@ -1248,6 +1140,7 @@ namespace Barotrauma new XAttribute("width", GraphicsWidth), new XAttribute("height", GraphicsHeight), new XAttribute("vsync", VSyncEnabled), + new XAttribute("compresstextures", TextureCompressionEnabled), new XAttribute("framelimit", Timing.FrameLimit), new XAttribute("displaymode", windowMode)); } @@ -1262,13 +1155,14 @@ namespace Barotrauma new XAttribute("musicvolume", musicVolume), new XAttribute("soundvolume", soundVolume), new XAttribute("voicechatvolume", voiceChatVolume), + new XAttribute("voicechatcutoffprevention", VoiceChatCutoffPrevention), new XAttribute("microphonevolume", microphoneVolume), new XAttribute("muteonfocuslost", MuteOnFocusLost), new XAttribute("dynamicrangecompressionenabled", DynamicRangeCompressionEnabled), new XAttribute("voipattenuationenabled", VoipAttenuationEnabled), new XAttribute("usedirectionalvoicechat", UseDirectionalVoiceChat), new XAttribute("voicesetting", VoiceSetting), - new XAttribute("voicecapturedevice", VoiceCaptureDevice ?? ""), + new XAttribute("voicecapturedevice", System.Xml.XmlConvert.EncodeName(VoiceCaptureDevice ?? "")), new XAttribute("noisegatethreshold", NoiseGateThreshold)); XElement gSettings = doc.Root.Element("graphicssettings"); @@ -1308,6 +1202,21 @@ namespace Barotrauma keyMappingElement.Add(new XAttribute(((InputType)i).ToString(), keyMapping[i].MouseButton)); } } + + var inventoryKeyMappingElement = new XElement("inventorykeymapping"); + doc.Root.Add(inventoryKeyMappingElement); + for (int i = 0; i < inventoryKeyMapping.Length; i++) + { + KeyOrMouse bind = inventoryKeyMapping[i]; + if (bind.MouseButton == MouseButton.None) + { + inventoryKeyMappingElement.Add(new XAttribute($"slot{i}", bind.Key)); + } + else + { + inventoryKeyMappingElement.Add(new XAttribute($"slot{i}", bind.MouseButton)); + } + } #endif var gameplay = new XElement("gameplay"); @@ -1352,7 +1261,7 @@ namespace Barotrauma } doc.Root.Add(tutorialElement); - XmlWriterSettings settings = new XmlWriterSettings + System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { Indent = true, OmitXmlDeclaration = true, @@ -1386,6 +1295,8 @@ namespace Barotrauma AutoCheckUpdates = doc.Root.GetAttributeBool("autocheckupdates", AutoCheckUpdates); sendUserStatistics = doc.Root.GetAttributeBool("senduserstatistics", sendUserStatistics); QuickStartSubmarineName = doc.Root.GetAttributeString("quickstartsub", QuickStartSubmarineName); + EnableSubmarineAutoSave = doc.Root.GetAttributeBool("submarineautosave", true); + SubEditorBackgroundColor = doc.Root.GetAttributeColor("subeditorbackground", new Color(0.051f, 0.149f, 0.271f, 1.0f)); UseSteamMatchmaking = doc.Root.GetAttributeBool("usesteammatchmaking", UseSteamMatchmaking); RequireSteamAuthentication = doc.Root.GetAttributeBool("requiresteamauthentication", RequireSteamAuthentication); EnableSplashScreen = doc.Root.GetAttributeBool("enablesplashscreen", EnableSplashScreen); @@ -1399,6 +1310,7 @@ namespace Barotrauma CampaignDisclaimerShown = doc.Root.GetAttributeBool("campaigndisclaimershown", CampaignDisclaimerShown); EditorDisclaimerShown = doc.Root.GetAttributeBool("editordisclaimershown", EditorDisclaimerShown); ShowTutorialSkipWarning = doc.Root.GetAttributeBool("tutorialskipwarning", true); + UseDualModeSockets = doc.Root.GetAttributeBool("usedualmodesockets", true); #if DEBUG AutomaticQuickStartEnabled = doc.Root.GetAttributeBool("automaticquickstartenabled", AutomaticQuickStartEnabled); TextManagerDebugModeEnabled = doc.Root.GetAttributeBool("textmanagerdebugmodeenabled", TextManagerDebugModeEnabled); @@ -1450,6 +1362,7 @@ namespace Barotrauma GraphicsWidth = graphicsMode.GetAttributeInt("width", GraphicsWidth); GraphicsHeight = graphicsMode.GetAttributeInt("height", GraphicsHeight); VSyncEnabled = graphicsMode.GetAttributeBool("vsync", VSyncEnabled); + TextureCompressionEnabled = graphicsMode.GetAttributeBool("compresstextures", TextureCompressionEnabled); Timing.FrameLimit = graphicsMode.GetAttributeInt("framelimit", 200); XElement graphicsSettings = doc.Root.Element("graphicssettings"); @@ -1487,10 +1400,11 @@ namespace Barotrauma DynamicRangeCompressionEnabled = audioSettings.GetAttributeBool("dynamicrangecompressionenabled", DynamicRangeCompressionEnabled); VoipAttenuationEnabled = audioSettings.GetAttributeBool("voipattenuationenabled", VoipAttenuationEnabled); VoiceChatVolume = audioSettings.GetAttributeFloat("voicechatvolume", VoiceChatVolume); + VoiceChatCutoffPrevention = audioSettings.GetAttributeInt("voicechatcutoffprevention", VoiceChatCutoffPrevention); MuteOnFocusLost = audioSettings.GetAttributeBool("muteonfocuslost", MuteOnFocusLost); UseDirectionalVoiceChat = audioSettings.GetAttributeBool("usedirectionalvoicechat", UseDirectionalVoiceChat); - VoiceCaptureDevice = audioSettings.GetAttributeString("voicecapturedevice", VoiceCaptureDevice); + VoiceCaptureDevice = System.Xml.XmlConvert.DecodeName(audioSettings.GetAttributeString("voicecapturedevice", VoiceCaptureDevice)); NoiseGateThreshold = audioSettings.GetAttributeFloat("noisegatethreshold", NoiseGateThreshold); MicrophoneVolume = audioSettings.GetAttributeFloat("microphonevolume", MicrophoneVolume); string voiceSettingStr = audioSettings.GetAttributeString("voicesetting", ""); @@ -1532,6 +1446,7 @@ namespace Barotrauma GraphicsWidth = 0; GraphicsHeight = 0; VSyncEnabled = true; + TextureCompressionEnabled = true; Timing.FrameLimit = 200; #if DEBUG EnableSplashScreen = false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/InputType.cs b/Barotrauma/BarotraumaShared/SharedSource/InputType.cs index addbfee1c..b3ae7c280 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/InputType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/InputType.cs @@ -13,6 +13,7 @@ namespace Barotrauma SelectNextCharacter, SelectPreviousCharacter, Voice, + LocalVoice, Deselect, Shoot, Command, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index e962781f8..9d70d1f58 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -150,7 +150,15 @@ namespace Barotrauma /// public override bool TryPutItem(Item item, Character user, List allowedSlots = null, bool createNetworkEvent = true) { - if (allowedSlots == null || !allowedSlots.Any()) return false; + if (allowedSlots == null || !allowedSlots.Any()) { return false; } + if (item == null) + { +#if DEBUG + throw new Exception("item null"); +#else + return false; +#endif + } bool inSuitableSlot = false; bool inWrongSlot = false; @@ -167,7 +175,7 @@ namespace Barotrauma } } //all good - if (inSuitableSlot && !inWrongSlot) return true; + if (inSuitableSlot && !inWrongSlot) { return true; } //try to place the item in a LimbSlot.Any slot if that's allowed if (allowedSlots.Contains(InvSlotType.Any) && item.AllowedSlots.Contains(InvSlotType.Any)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index b320d792d..5764c311c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -5,7 +5,7 @@ using FarseerPhysics.Dynamics.Joints; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; @@ -29,6 +29,7 @@ namespace Barotrauma.Items.Components private Door door; private Body[] bodies; + private Fixture outsideBlocker; private Body doorBody; private bool docked; @@ -58,7 +59,7 @@ namespace Barotrauma.Items.Components set; } - [Serialize(false, false, description: "If set to true, this docking port is used when spawning the submarine docked to an outpost (if possible).")] + [Editable, Serialize(false, true, description: "If set to true, this docking port is used when spawning the submarine docked to an outpost (if possible).")] public bool MainDockingPort { get; @@ -113,6 +114,12 @@ namespace Barotrauma.Items.Components { if (DockingTarget != null) { + if (IsHorizontal) + { + DockingDir = 0; + DockingDir = GetDir(DockingTarget); + DockingTarget.DockingDir = -DockingDir; + } if (joint != null) { CreateJoint(joint is WeldJoint); @@ -259,6 +266,12 @@ namespace Barotrauma.Items.Components { item.CreateServerEvent(this); } +#else + if (GameMain.Client != null && GameMain.Client.MidRoundSyncing && + (item.Submarine == Submarine.MainSub || DockingTarget.item.Submarine == Submarine.MainSub)) + { + Screen.Selected.Cam.Position = Submarine.MainSub.WorldPosition; + } #endif } @@ -461,6 +474,12 @@ namespace Barotrauma.Items.Components } } + if (leftSubRightSide == int.MinValue || rightSubLeftSide == int.MaxValue) + { + DebugConsole.NewMessage("Creating hulls between docking ports failed. Could not find a hull next to the docking port."); + return; + } + //expand left hull to the rightmost hull of the sub at the left side //(unless the difference is more than 100 units - if the distance is very large //there's something wrong with the positioning of the docking ports or submarine hulls) @@ -469,7 +488,8 @@ namespace Barotrauma.Items.Components { if (leftHullDiff > 100) { - DebugConsole.ThrowError("Creating hulls between docking ports failed. The leftmost docking port seems to be very far from any hulls in the left-side submarine."); + DebugConsole.NewMessage("Creating hulls between docking ports failed. The leftmost docking port seems to be very far from any hulls in the left-side submarine."); + return; } else { @@ -483,7 +503,8 @@ namespace Barotrauma.Items.Components { if (rightHullDiff > 100) { - DebugConsole.ThrowError("Creating hulls between docking ports failed. The rightmost docking port seems to be very far from any hulls in the right-side submarine."); + DebugConsole.NewMessage("Creating hulls between docking ports failed. The rightmost docking port seems to be very far from any hulls in the right-side submarine."); + return; } else { @@ -506,6 +527,16 @@ namespace Barotrauma.Items.Components } } + if (rightHullDiff <= 100 && hulls[0].Submarine != null) + { + outsideBlocker = hulls[0].Submarine.PhysicsBody.FarseerBody.CreateRectangle( + ConvertUnits.ToSimUnits(hullRects[0].Width + hullRects[1].Width), + ConvertUnits.ToSimUnits(hullRects[0].Height), + density: 0.0f, + offset: ConvertUnits.ToSimUnits(new Vector2(hullRects[0].Right, hullRects[0].Y - hullRects[0].Height / 2) - hulls[0].Submarine.HiddenSubPosition)); + outsideBlocker.UserData = this; + } + gap = new Gap(new Rectangle(hullRects[0].Right - 2, hullRects[0].Y, 4, hullRects[0].Height), true, subs[0]); } else @@ -540,6 +571,12 @@ namespace Barotrauma.Items.Components } } + if (upperSubBottom == int.MaxValue || lowerSubTop == int.MinValue) + { + DebugConsole.NewMessage("Creating hulls between docking ports failed. Could not find a hull next to the docking port."); + return; + } + //expand lower hull to the topmost hull of the lower sub //(unless the difference is more than 100 units - if the distance is very large //there's something wrong with the positioning of the docking ports or submarine hulls) @@ -548,7 +585,8 @@ namespace Barotrauma.Items.Components { if (lowerHullDiff > 100) { - DebugConsole.ThrowError("Creating hulls between docking ports failed. The lower docking port seems to be very far from any hulls in the lower submarine."); + DebugConsole.NewMessage("Creating hulls between docking ports failed. The lower docking port seems to be very far from any hulls in the lower submarine."); + return; } else { @@ -561,7 +599,8 @@ namespace Barotrauma.Items.Components { if (upperHullDiff > 100) { - DebugConsole.ThrowError("Creating hulls between docking ports failed. The upper docking port seems to be very far from any hulls in the upper submarine."); + DebugConsole.NewMessage("Creating hulls between docking ports failed. The upper docking port seems to be very far from any hulls in the upper submarine."); + return; } else { @@ -575,7 +614,8 @@ namespace Barotrauma.Items.Components int midHullDiff = ((hullRects[1].Y - hullRects[1].Height) - hullRects[0].Y) + 2; if (midHullDiff > 100) { - DebugConsole.ThrowError("Creating hulls between docking ports failed. The upper hull seems to be very far from the lower hull."); + DebugConsole.NewMessage("Creating hulls between docking ports failed. The upper hull seems to be very far from the lower hull."); + return; } else if (midHullDiff > 0) { @@ -584,15 +624,33 @@ namespace Barotrauma.Items.Components hullRects[1].Height += midHullDiff / 2 + 1; } + for (int i = 0; i < 2; i++) { hullRects[i].Location -= MathUtils.ToPoint((subs[i].WorldPosition - subs[i].HiddenSubPosition)); hulls[i] = new Hull(MapEntityPrefab.Find(null, "hull"), hullRects[i], subs[i]); hulls[i].AddToGrid(subs[i]); hulls[i].FreeID(); + + for (int j = 0; j < 2; j++) + { + bodies[i + j * 2] = GameMain.World.CreateEdge( + ConvertUnits.ToSimUnits(new Vector2(hullRects[i].X + hullRects[i].Width * j, hullRects[i].Y)), + ConvertUnits.ToSimUnits(new Vector2(hullRects[i].X + hullRects[i].Width * j, hullRects[i].Y - hullRects[i].Height))); + } } - gap = new Gap(new Rectangle(hullRects[0].X, hullRects[0].Y+2, hullRects[0].Width, 4), false, subs[0]); + if (midHullDiff <= 100 && hulls[0].Submarine != null) + { + outsideBlocker = hulls[0].Submarine.PhysicsBody.FarseerBody.CreateRectangle( + ConvertUnits.ToSimUnits(hullRects[0].Width), + ConvertUnits.ToSimUnits(hullRects[0].Height + hullRects[1].Height), + density: 0.0f, + offset: ConvertUnits.ToSimUnits(new Vector2(hullRects[0].Center.X, hullRects[0].Y) - hulls[0].Submarine.HiddenSubPosition)); + outsideBlocker.UserData = this; + } + + gap = new Gap(new Rectangle(hullRects[0].X, hullRects[0].Y + 2, hullRects[0].Width, 4), false, subs[0]); } LinkHullsToGaps(); @@ -609,7 +667,7 @@ namespace Barotrauma.Items.Components foreach (Body body in bodies) { - if (body == null) continue; + if (body == null) { continue; } body.BodyType = BodyType.Static; body.Friction = 0.5f; @@ -769,6 +827,9 @@ namespace Barotrauma.Items.Components bodies = null; } + outsideBlocker?.Body.Remove(outsideBlocker); + outsideBlocker = null; + Item.Submarine.EnableObstructedWaypoints(); obstructedWayPointsDisabled = false; @@ -880,12 +941,12 @@ namespace Barotrauma.Items.Components float closestDist = 30.0f * 30.0f; foreach (Item it in Item.ItemList) { - if (it.Submarine != item.Submarine) continue; + if (it.Submarine != item.Submarine) { continue; } var doorComponent = it.GetComponent(); - if (doorComponent == null) continue; + if (doorComponent == null || doorComponent.IsHorizontal == IsHorizontal) { continue; } - float distSqr = Vector2.Distance(item.Position, it.Position); + float distSqr = Vector2.DistanceSquared(item.Position, it.Position); if (distSqr < closestDist) { door = doorComponent; @@ -958,12 +1019,12 @@ namespace Barotrauma.Items.Components if (docked) { if (item.Submarine != null && DockingTarget?.item?.Submarine != null) - GameServer.Log(sender.LogName + " docked " + item.Submarine.Info.Name + " to " + DockingTarget.item.Submarine.Info.Name, ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(sender) + " docked " + item.Submarine.Info.Name + " to " + DockingTarget.item.Submarine.Info.Name, ServerLog.MessageType.ItemInteraction); } else { if (item.Submarine != null && prevDockingTarget?.item?.Submarine != null) - GameServer.Log(sender.LogName + " undocked " + item.Submarine.Info.Name + " from " + prevDockingTarget.item.Submarine.Info.Name, ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(sender) + " undocked " + item.Submarine.Info.Name + " from " + prevDockingTarget.item.Submarine.Info.Name, ServerLog.MessageType.ItemInteraction); } } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index 45d565f4c..0d766b44c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -4,7 +4,7 @@ using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; #if CLIENT @@ -38,12 +38,17 @@ namespace Barotrauma.Items.Components } } + //how much "less stuck" partially doors get when opened + const float StuckReductionOnOpen = 30.0f; + private float resetPredictionTimer; private float toggleCooldownTimer; private Character lastUser; private float damageSoundCooldown; + private double lastBrokenTime; + private Rectangle doorRect; private bool isBroken; @@ -53,7 +58,7 @@ namespace Barotrauma.Items.Components get { return isBroken; } set { - if (isBroken == value) return; + if (isBroken == value) { return; } isBroken = value; if (isBroken) { @@ -63,6 +68,9 @@ namespace Barotrauma.Items.Components { EnableBody(); } +#if SERVER + item.CreateServerEvent(this); +#endif } } @@ -85,7 +93,7 @@ namespace Barotrauma.Items.Components if (isOpen || isBroken || !CanBeWelded) return; stuck = MathHelper.Clamp(value, 0.0f, 100.0f); if (stuck <= 0.0f) { IsStuck = false; } - if (stuck >= 100.0f) { IsStuck = true; } + if (stuck >= 99.0f) { IsStuck = true; } } } @@ -203,10 +211,16 @@ namespace Barotrauma.Items.Components break; } } + + IsActive = true; + } + public override void OnItemLoaded() + { + //do this here because the scale of the item might not be set to the final value yet in the constructor doorRect = new Rectangle( item.Rect.Center.X - (int)(doorSprite.size.X / 2 * item.Scale), - item.Rect.Y - item.Rect.Height/2 + (int)(doorSprite.size.Y / 2.0f * item.Scale), + item.Rect.Y - item.Rect.Height / 2 + (int)(doorSprite.size.Y / 2.0f * item.Scale), (int)(doorSprite.size.X * item.Scale), (int)(doorSprite.size.Y * item.Scale)); @@ -224,8 +238,6 @@ namespace Barotrauma.Items.Components Body.SetTransformIgnoreContacts( ConvertUnits.ToSimUnits(new Vector2(doorRect.Center.X, doorRect.Y - doorRect.Height / 2)), 0.0f); - - IsActive = true; } public override void Move(Vector2 amount) @@ -295,6 +307,7 @@ namespace Barotrauma.Items.Components PickingTime = 0; ToggleState(ActionType.OnUse, character); PickingTime = originalPickingTime; + StopPicking(picker); } #if CLIENT else if (hasRequiredItems && character != null && character == Character.Controlled) @@ -313,8 +326,9 @@ namespace Barotrauma.Items.Components if (isBroken) { + lastBrokenTime = Timing.TotalTime; //the door has to be restored to 50% health before collision detection on the body is re-enabled - if (item.ConditionPercentage > 50.0f) + if (item.ConditionPercentage > 50.0f && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)) { IsBroken = false; } @@ -363,7 +377,10 @@ namespace Barotrauma.Items.Components public override void UpdateBroken(float deltaTime, Camera cam) { base.UpdateBroken(deltaTime, cam); - IsBroken = true; + if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) + { + IsBroken = true; + } } private void EnableBody() @@ -502,6 +519,7 @@ namespace Barotrauma.Items.Components foreach (Limb limb in c.AnimController.Limbs) { + if (limb.IsSevered) { continue; } if (PushBodyOutOfDoorway(c, limb.body, dir, simPos, simSize) && damageSoundCooldown <= 0.0f) { #if CLIENT @@ -564,7 +582,12 @@ namespace Barotrauma.Items.Components body.ApplyLinearImpulse(new Vector2(dir * 2.0f, isOpen ? 0.0f : -1.0f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity); } - c.SetStun(0.2f); + //don't stun if the door was broken a moment ago + //otherwise enabling the door's collider and pushing the character away will interrupt repairing + if (lastBrokenTime < Timing.TotalTime - 1.0f) + { + c.SetStun(0.2f); + } return true; } @@ -594,7 +617,7 @@ namespace Barotrauma.Items.Components #if SERVER if (sender != null && wasOpen != isOpen) { - GameServer.Log(sender.LogName + (isOpen ? " opened " : " closed ") + item.Name, ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(sender) + (isOpen ? " opened " : " closed ") + item.Name, ServerLog.MessageType.ItemInteraction); } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs index e08385827..582282b04 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs @@ -1,13 +1,13 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Xml.Linq; namespace Barotrauma.Items.Components { - partial class ElectricalDischarger : Powered + partial class ElectricalDischarger : Powered, IServerSerializable { private static readonly List list = new List(); public static IEnumerable List @@ -48,14 +48,14 @@ namespace Barotrauma.Items.Components } } - [Serialize(500.0f, true, description: "How far the discharge can travel from the item."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 5000.0f)] + [Serialize(500.0f, true, description: "How far the discharge can travel from the item.", alwaysUseInstanceValues: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 5000.0f)] public float Range { get; set; } - [Serialize(25.0f, true, description: "How much further can the discharge be carried when moving across walls."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] + [Serialize(25.0f, true, description: "How much further can the discharge be carried when moving across walls.", alwaysUseInstanceValues: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] public float RangeMultiplierInWalls { get; @@ -115,10 +115,15 @@ namespace Barotrauma.Items.Components //already active, do nothing if (IsActive) { return false; } + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; } + CurrPowerConsumption = powerConsumption; charging = true; timer = Duration; IsActive = true; +#if SERVER + if (GameMain.Server != null) { item.CreateServerEvent(this); } +#endif return false; } @@ -150,14 +155,12 @@ namespace Barotrauma.Items.Components neededPower -= takePower; battery.Charge -= takePower / 3600.0f; #if SERVER - if (GameMain.Server != null) - { - battery.Item.CreateServerEvent(battery); - } + if (GameMain.Server != null) { battery.Item.CreateServerEvent(battery); } #endif } } Discharge(); + } else if (Voltage > MinVoltage) { @@ -478,5 +481,10 @@ namespace Barotrauma.Items.Components base.RemoveComponentSpecific(); list.Remove(this); } + + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + { + //no further data needed, the event just triggers the discharge + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index 6e9d8981d..3ea347165 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -3,6 +3,7 @@ using FarseerPhysics; using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics.Contacts; using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; using System.Xml.Linq; @@ -309,8 +310,13 @@ namespace Barotrauma.Items.Components { picker = character; - if (character != null) item.Submarine = character.Submarine; + if (item.Removed) + { + DebugConsole.ThrowError($"Attempted to equip a removed item ({item.Name})\n" + Environment.StackTrace); + return; + } + if (character != null) { item.Submarine = character.Submarine; } if (item.body == null) { if (body != null) @@ -344,7 +350,7 @@ namespace Barotrauma.Items.Components IsActive = true; #if SERVER - if (!alreadyEquipped) GameServer.Log(character.LogName + " equipped " + item.Name, ServerLog.MessageType.ItemInteraction); + if (!alreadyEquipped) GameServer.Log(GameServer.CharacterLogName(character) + " equipped " + item.Name, ServerLog.MessageType.ItemInteraction); #endif } } @@ -355,7 +361,7 @@ namespace Barotrauma.Items.Components picker.DeselectItem(item); #if SERVER - GameServer.Log(character.LogName + " unequipped " + item.Name, ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(character) + " unequipped " + item.Name, ServerLog.MessageType.ItemInteraction); #endif item.body.PhysEnabled = true; @@ -365,23 +371,30 @@ namespace Barotrauma.Items.Components public bool CanBeAttached() { - if (!attachable || !Reattachable) return false; + if (!attachable || !Reattachable) { return false; } //can be attached anywhere in sub editor - if (Screen.Selected == GameMain.SubEditorScreen) return true; + if (Screen.Selected == GameMain.SubEditorScreen) { return true; } //can be attached anywhere inside hulls - if (item.CurrentHull != null) return true; + if (item.CurrentHull != null) { return true; } return Structure.GetAttachTarget(item.WorldPosition) != null; } public bool CanBeDeattached() { - if (!attachable || !attached) return true; + if (!attachable || !attached) { return true; } //allow deattaching everywhere in sub editor - if (Screen.Selected == GameMain.SubEditorScreen) return true; + if (Screen.Selected == GameMain.SubEditorScreen) { return true; } + + //if the item has a connection panel and rewiring is disabled, don't allow deattaching + var connectionPanel = item.GetComponent(); + if (connectionPanel != null && (connectionPanel.Locked || !(GameMain.NetworkMember?.ServerSettings?.AllowRewiring ?? true))) + { + return false; + } //don't allow deattaching if part of a sub and outside hulls return item.Submarine == null || item.CurrentHull != null; @@ -389,12 +402,18 @@ namespace Barotrauma.Items.Components public override bool Pick(Character picker) { + if (item.Removed) + { + DebugConsole.ThrowError($"Attempted to pick up a removed item ({item.Name})\n" + Environment.StackTrace); + return false; + } + if (!attachable) { return base.Pick(picker); } - if (!CanBeDeattached()) return false; + if (!CanBeDeattached()) { return false; } if (Attached) { @@ -419,7 +438,7 @@ namespace Barotrauma.Items.Components item.CreateServerEvent(this); if (picker != null) { - GameServer.Log(picker.LogName + " detached " + item.Name + " from a wall", ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(picker) + " detached " + item.Name + " from a wall", ServerLog.MessageType.ItemInteraction); } } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index 0f575f5d1..8f44ebff5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -1,4 +1,5 @@ -using FarseerPhysics; +using Barotrauma.Networking; +using FarseerPhysics; using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics.Contacts; using Microsoft.Xna.Framework; @@ -94,6 +95,7 @@ namespace Barotrauma.Items.Components { foreach (Limb l in character.AnimController.Limbs) { + if (l.IsSevered) { continue; } if (l.type == LimbType.LeftFoot || l.type == LimbType.LeftThigh || l.type == LimbType.LeftLeg) { continue; } if (l.type == LimbType.Head || l.type == LimbType.Torso) { @@ -310,70 +312,7 @@ namespace Barotrauma.Items.Components return false; } - if (attack != null) - { - if (targetLimb == null && targetCharacter == null && targetStructure == null && (targetItem == null || ! targetItem.Prefab.DamagedByMeleeWeapons)) - { - return false; - } - - if (targetLimb != null) - { - targetLimb.character.LastDamageSource = item; - attack.DoDamageToLimb(User, targetLimb, item.WorldPosition, 1.0f); - } - else if (targetCharacter != null) - { - targetCharacter.LastDamageSource = item; - attack.DoDamage(User, targetCharacter, item.WorldPosition, 1.0f); - } - else if (targetStructure != null) - { - 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; - } - } - - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return true; } - -#if SERVER - if (GameMain.Server != null && targetCharacter != null) //TODO: Log structure hits - { - - GameMain.Server.CreateEntityEvent(item, new object[] - { - Networking.NetEntityEvent.Type.ApplyStatusEffect, - ActionType.OnUse, - null, //itemcomponent - targetCharacter.ID, targetLimb - }); - - string logStr = picker?.LogName + " used " + item.Name; - if (item.ContainedItems != null && item.ContainedItems.Any()) - { - logStr += " (" + string.Join(", ", item.ContainedItems.Select(i => i?.Name)) + ")"; - } - logStr += " on " + targetCharacter.LogName + "."; - Networking.GameServer.Log(logStr, Networking.ServerLog.MessageType.Attack); - } -#endif - - if (targetCharacter != null) //TODO: Allow OnUse to happen on structures too maybe?? - { - ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, user: User); - } - - if (DeleteOnUse) - { - Entity.Spawner.AddToRemoveQueue(item); - } + impactQueue.Enqueue(f2); return true; } @@ -414,7 +353,7 @@ namespace Barotrauma.Items.Components if (targetStructure.Removed) { return; } attack.DoDamage(User, targetStructure, item.WorldPosition, 1.0f); } - else if (targetItem != null && targetItem.Prefab.DamagedByMeleeWeapons) + else if (targetItem != null && targetItem.Prefab.DamagedByMeleeWeapons && targetItem.Condition > 0) { if (targetItem.Removed) { return; } attack.DoDamage(User, targetItem, item.WorldPosition, 1.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs index f9a0c0199..72012a444 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs @@ -65,7 +65,7 @@ namespace Barotrauma.Items.Components if (PickingTime > 0.0f) { - if (picker.PickingItem == null && PickingTime <= float.MaxValue) + if ((picker.PickingItem == null || picker.PickingItem == item) && PickingTime <= float.MaxValue) { #if SERVER item.CreateServerEvent(this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs index e8060e3ba..606dc01c2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Propulsion.cs @@ -65,7 +65,7 @@ namespace Barotrauma.Items.Components foreach (Limb limb in character.AnimController.Limbs) { - if (limb.WearingItems.Find(w => w.WearableComponent.Item == this.item) == null) continue; + if (limb.WearingItems.Find(w => w.WearableComponent.Item == item) == null) { continue; } limb.body.ApplyForce(propulsion, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index aa299a0a0..b7f3a36b9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -90,6 +90,7 @@ namespace Barotrauma.Items.Components return MathHelper.ToRadians(MathHelper.Lerp(Spread, UnskilledSpread, degreeOfFailure)); } + private readonly List limbBodies = new List(); public override bool Use(float deltaTime, Character character = null) { if (character == null || character.Removed) { return false; } @@ -104,9 +105,10 @@ namespace Barotrauma.Items.Components item.AiTarget.SightRange = item.AiTarget.MaxSightRange; } - List limbBodies = new List(); + limbBodies.Clear(); foreach (Limb l in character.AnimController.Limbs) { + if (l.IsSevered) { continue; } limbBodies.Add(l.body.FarseerBody); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index 7d7638c33..8312209bf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -22,6 +22,8 @@ namespace Barotrauma.Items.Components private Vector2 debugRayStartPos, debugRayEndPos; + private readonly List ignoredBodies = new List(); + [Serialize("Both", false, description: "Can the item be used in air, water or both.")] public UseEnvironment UsableIn { @@ -114,8 +116,7 @@ namespace Barotrauma.Items.Components } } item.IsShootable = true; - // TODO: should define this in xml if we have repair tools that don't require aim to use - item.RequireAimToUse = true; + item.RequireAimToUse = element.Parent.GetAttributeBool("requireaimtouse", true); InitProjSpecific(element); } @@ -124,16 +125,17 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { activeTimer -= deltaTime; - if (activeTimer <= 0.0f) IsActive = false; + 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; - if (item.RequireAimToUse && !character.IsKeyDown(InputType.Aim)) return false; + if (character != null) + { + if (item.RequireAimToUse && !character.IsKeyDown(InputType.Aim)) { return false; } + } - float degreeOfSuccess = DegreeOfSuccess(character); + float degreeOfSuccess = character == null ? 0.5f : DegreeOfSuccess(character); if (Rand.Range(0.0f, 0.5f) > degreeOfSuccess) { @@ -187,12 +189,15 @@ namespace Barotrauma.Items.Components (float)Math.Sin(angle)) * Range * item.body.Dir); ignoredBodies.Clear(); - foreach (Limb limb in character.AnimController.Limbs) + if (character != null) { - if (Rand.Range(0.0f, 0.5f) > degreeOfSuccess) continue; - ignoredBodies.Add(limb.body.FarseerBody); + foreach (Limb limb in character.AnimController.Limbs) + { + if (Rand.Range(0.0f, 0.5f) > degreeOfSuccess) continue; + ignoredBodies.Add(limb.body.FarseerBody); + } + ignoredBodies.Add(character.AnimController.Collider.FarseerBody); } - ignoredBodies.Add(character.AnimController.Collider.FarseerBody); IsActive = true; activeTimer = 0.1f; @@ -200,7 +205,8 @@ namespace Barotrauma.Items.Components debugRayStartPos = ConvertUnits.ToDisplayUnits(rayStart); debugRayEndPos = ConvertUnits.ToDisplayUnits(rayEnd); - if (character.Submarine == null) + Submarine parentSub = character?.Submarine ?? item.Submarine; + if (parentSub == null) { foreach (Submarine sub in Submarine.Loaded) { @@ -216,7 +222,7 @@ namespace Barotrauma.Items.Components } else { - Repair(rayStart - character.Submarine.SimPosition, rayEnd - character.Submarine.SimPosition, deltaTime, character, degreeOfSuccess, ignoredBodies); + Repair(rayStart - parentSub.SimPosition, rayEnd - parentSub.SimPosition, deltaTime, character, degreeOfSuccess, ignoredBodies); } UseProjSpecific(deltaTime, rayStart); @@ -439,18 +445,7 @@ namespace Barotrauma.Items.Components return true; } else if (targetBody.UserData is Item targetItem) - { - targetItem.IsHighlighted = true; - - ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, targetItem.AllPropertyObjects); - - if (targetItem.body != null && !MathUtils.NearlyEqual(TargetForce, 0.0f)) - { - Vector2 dir = targetItem.WorldPosition - item.WorldPosition; - dir = dir.LengthSquared() < 0.0001f ? Vector2.UnitY : Vector2.Normalize(dir); - targetItem.body.ApplyForce(dir * TargetForce, maxVelocity: 10.0f); - } - + { var levelResource = targetItem.GetComponent(); if (levelResource != null && levelResource.Attached && levelResource.requiredItems.Any() && @@ -464,7 +459,23 @@ namespace Barotrauma.Items.Components levelResource.DeattachTimer / levelResource.DeattachDuration, GUI.Style.Red, GUI.Style.Green); #endif + return true; } + + if (!targetItem.Prefab.DamagedByRepairTools) { return false; } + if (item.GetComponent() == null && item.Condition <= 0) { return false; } + + targetItem.IsHighlighted = true; + + ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, targetItem.AllPropertyObjects); + + if (targetItem.body != null && !MathUtils.NearlyEqual(TargetForce, 0.0f)) + { + Vector2 dir = targetItem.WorldPosition - item.WorldPosition; + dir = dir.LengthSquared() < 0.0001f ? Vector2.UnitY : Vector2.Normalize(dir); + targetItem.body.ApplyForce(dir * TargetForce, maxVelocity: 10.0f); + } + FixItemProjSpecific(user, deltaTime, targetItem); return true; } @@ -642,26 +653,26 @@ namespace Barotrauma.Items.Components } #if CLIENT + if (user == null) { return; } // Hard-coded progress bars for welding doors stuck. // A general purpose system could be better, but it would most likely require changes in the way we define the status effects in xml. foreach (ISerializableEntity target in targets) { - if (target is Door door) + if (!(target is Door door)) { continue; } + + if (!door.CanBeWelded) { continue; } + for (int i = 0; i < effect.propertyNames.Length; i++) { - if (!door.CanBeWelded) continue; - for (int i = 0; i < effect.propertyNames.Length; i++) + string propertyName = effect.propertyNames[i]; + if (propertyName != "stuck") { continue; } + if (door.SerializableProperties == null || !door.SerializableProperties.TryGetValue(propertyName, out SerializableProperty property)) { continue; } + object value = property.GetValue(target); + if (door.Stuck > 0) { - string propertyName = effect.propertyNames[i]; - if (propertyName != "stuck") { continue; } - if (door.SerializableProperties == null || !door.SerializableProperties.TryGetValue(propertyName, out SerializableProperty property)) { continue; } - object value = property.GetValue(target); - if (door.Stuck > 0) - { - var progressBar = user.UpdateHUDProgressBar(door, door.Item.WorldPosition, door.Stuck / 100, Color.DarkGray * 0.5f, Color.White); - if (progressBar != null) { progressBar.Size = new Vector2(60.0f, 20.0f); } - } + var progressBar = user.UpdateHUDProgressBar(door, door.Item.WorldPosition, door.Stuck / 100, Color.DarkGray * 0.5f, Color.White); + if (progressBar != null) { progressBar.Size = new Vector2(60.0f, 20.0f); } } - } + } } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs index 317b5c9d0..b50e7c525 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs @@ -115,7 +115,7 @@ namespace Barotrauma.Items.Components if (!MathUtils.IsValid(throwVector)) { throwVector = Vector2.UnitY; } #if SERVER - GameServer.Log(picker.LogName + " threw " + item.Name, ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(picker) + " threw " + item.Name, ServerLog.MessageType.ItemInteraction); #endif Character thrower = picker; item.Drop(thrower, createNetworkEvent: GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index 8faa8e1b4..e907e3610 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -421,7 +421,10 @@ namespace Barotrauma.Items.Components case "activate": case "use": case "trigger_in": - item.Use(1.0f, sender); + if (signal != "0") + { + item.Use(1.0f, sender); + } break; case "toggle": if (signal != "0") @@ -734,12 +737,15 @@ namespace Barotrauma.Items.Components public virtual void Load(XElement componentElement, bool usePrefabValues) { - if (componentElement != null && !usePrefabValues) + if (componentElement != null) { foreach (XAttribute attribute in componentElement.Attributes()) { if (!SerializableProperties.TryGetValue(attribute.Name.ToString().ToLowerInvariant(), out SerializableProperty property)) { continue; } - property.TrySetValue(this, attribute.Value); + if (property.OverridePrefabValues || !usePrefabValues) + { + property.TrySetValue(this, attribute.Value); + } } ParseMsg(); OverrideRequiredItems(componentElement); @@ -908,49 +914,56 @@ namespace Barotrauma.Items.Components #region AI related protected const float AIUpdateInterval = 0.2f; protected float aiUpdateTimer; - private int itemIndex; - private List ignoredContainers = new List(); private Character previousUser; protected bool FindSuitableContainer(Character character, Func priority, out Item suitableContainer) { - if (previousUser != character) - { - ignoredContainers.Clear(); - previousUser = character; - } suitableContainer = null; - if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: ignoredContainers, customPriorityFunction: priority)) + if (character.AIController is HumanAIController aiController) { - suitableContainer = targetContainer; - return true; + if (previousUser != character) + { + previousUser = character; + itemIndex = 0; + } + if (character.FindItem(ref itemIndex, out Item targetContainer, ignoredItems: aiController.IgnoredItems, customPriorityFunction: priority)) + { + suitableContainer = targetContainer; + return true; + } } return false; } protected AIObjectiveContainItem AIContainItems(ItemContainer container, Character character, AIObjective objective, int itemCount, bool equip, bool removeEmpty) where T : ItemComponent { - var containObjective = new AIObjectiveContainItem(character, container.GetContainableItemIdentifiers.ToArray(), container, objective.objectiveManager) + AIObjectiveContainItem containObjective = null; + if (character.AIController is HumanAIController aiController) { - targetItemCount = itemCount, - Equip = equip, - RemoveEmpty = removeEmpty, - GetItemPriority = i => + containObjective = new AIObjectiveContainItem(character, container.GetContainableItemIdentifiers.ToArray(), container, objective.objectiveManager) { - if (i.ParentInventory?.Owner is Item) + targetItemCount = itemCount, + Equip = equip, + RemoveEmpty = removeEmpty, + GetItemPriority = i => { - //don't take items from other items of the same type - if (((Item)i.ParentInventory.Owner).GetComponent() != null) + if (i.ParentInventory?.Owner is Item) { - return 0.0f; + //don't take items from other items of the same type + if (((Item)i.ParentInventory.Owner).GetComponent() != null) + { + return 0.0f; + } } + return 1.0f; } - return 1.0f; - } - }; - // TODO: are we sure that we want to abandon the objective here? - containObjective.Abandoned += () => objective.Abandon = true; - objective.AddSubObjective(containObjective); + }; + containObjective.Abandoned += () => + { + aiController.IgnoredItems.Add(container.Item); + }; + objective.AddSubObjective(containObjective); + } return containObjective; } @@ -959,68 +972,71 @@ namespace Barotrauma.Items.Components /// protected bool AIDecontainEmptyItems(Character character, AIObjective objective, bool equip, ItemContainer sourceContainer = null) { - ItemContainer sourceC = sourceContainer ?? (item.OwnInventory?.Owner is Item it ? it.GetComponent() : null); - var containedItems = sourceContainer != null ? sourceContainer.Inventory.Items : item.OwnInventory.Items; - foreach (Item containedItem in containedItems) + if (character.AIController is HumanAIController aiController) { - if (containedItem != null && containedItem.Condition <= 0.0f) + ItemContainer sourceC = sourceContainer ?? (item.OwnInventory?.Owner is Item it ? it.GetComponent() : null); + var containedItems = sourceContainer != null ? sourceContainer.Inventory.Items : item.OwnInventory.Items; + foreach (Item containedItem in containedItems) { - if (FindSuitableContainer(character, - i => - { - var container = i.GetComponent(); - if (container == null) { return 0; } - if (container.Inventory.IsFull()) { return 0; } + if (containedItem != null && containedItem.Condition <= 0.0f) + { + if (FindSuitableContainer(character, + i => + { + var container = i.GetComponent(); + if (container == null) { return 0; } + if (container.Inventory.IsFull()) { return 0; } // Ignore containers that are identical to the source container if (sourceC != null && container.Item.Prefab == sourceC.Item.Prefab) { return 0; } - if (container.ShouldBeContained(containedItem, out bool isRestrictionsDefined)) - { - if (isRestrictionsDefined) + if (container.ShouldBeContained(containedItem, out bool isRestrictionsDefined)) { - return 4; - } - else - { - if (containedItem.Prefab.IsContainerPreferred(container, out bool isPreferencesDefined, out bool isSecondary)) + if (isRestrictionsDefined) { - return isPreferencesDefined ? isSecondary ? 2 : 3 : 1; + return 4; } else { - return isPreferencesDefined ? 0 : 1; + if (containedItem.Prefab.IsContainerPreferred(container, out bool isPreferencesDefined, out bool isSecondary)) + { + return isPreferencesDefined ? isSecondary ? 2 : 3 : 1; + } + else + { + return isPreferencesDefined ? 0 : 1; + } } } - } - else + else + { + return 0; + } + }, out Item targetContainer)) + { + var decontainObjective = new AIObjectiveDecontainItem(character, containedItem, objective.objectiveManager, sourceC, targetContainer?.GetComponent()) { - return 0; - } - }, out Item targetContainer)) - { - var decontainObjective = new AIObjectiveDecontainItem(character, containedItem, objective.objectiveManager, sourceC, targetContainer?.GetComponent()) - { - Equip = equip - }; - decontainObjective.Abandoned += () => - { - itemIndex = 0; - if (targetContainer != null) - { - ignoredContainers.Add(targetContainer); - } - }; - decontainObjective.Completed += () => - { - if (targetContainer == null) + Equip = equip + }; + decontainObjective.Abandoned += () => { itemIndex = 0; - } - }; - objective.AddSubObjectiveInQueue(decontainObjective); - } - else - { - return false; + if (targetContainer != null) + { + aiController.IgnoredItems.Add(targetContainer); + } + }; + decontainObjective.Completed += () => + { + if (targetContainer == null) + { + itemIndex = 0; + } + }; + objective.AddSubObjectiveInQueue(decontainObjective); + } + else + { + return false; + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index 4bea6fd32..78210b5d1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -80,6 +80,13 @@ namespace Barotrauma.Items.Components set { itemRotation = MathHelper.ToRadians(value); } } + [Serialize("", false, description: "Specify an item for the container to spawn with.")] + public string SpawnWithId + { + get; + set; + } + public bool ShouldBeContained(string[] identifiersOrTags, out bool isRestrictionsDefined) { isRestrictionsDefined = containableRestrictions.Any(); @@ -143,7 +150,7 @@ namespace Barotrauma.Items.Components } //no need to Update() if this item has no statuseffects and no physics body - IsActive = itemsWithStatusEffects.Count > 0 || containedItem.body != null; + IsActive = itemsWithStatusEffects.Count > 0 || Inventory.Items.Any(it => it?.body != null); } public void OnItemRemoved(Item containedItem) @@ -151,7 +158,7 @@ namespace Barotrauma.Items.Components itemsWithStatusEffects.RemoveAll(i => i.First == containedItem); //deactivate if the inventory is empty - IsActive = itemsWithStatusEffects.Count > 0 || containedItem.body != null; + IsActive = itemsWithStatusEffects.Count > 0 || Inventory.Items.Any(it => it?.body != null); } public bool CanBeContained(Item item) @@ -200,6 +207,22 @@ namespace Barotrauma.Items.Components } } + public override void OnItemLoaded() + { + base.OnItemLoaded(); + if (SpawnWithId.Length > 0) + { + ItemPrefab prefab = ItemPrefab.Prefabs.Find(m => m.Identifier == SpawnWithId); + if (prefab != null) + { + if (Inventory != null && Inventory.Items.Any(it => it == null)) + { + Entity.Spawner?.AddToSpawnQueue(prefab, Inventory); + } + } + } + } + public override bool HasRequiredItems(Character character, bool addMessage, string msg = null) { return (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg); @@ -295,7 +318,7 @@ namespace Barotrauma.Items.Components foreach (Item contained in Inventory.Items) { - if (contained == null) continue; + if (contained == null) { continue; } if (contained.body != null) { try @@ -311,6 +334,7 @@ namespace Barotrauma.Items.Components GameAnalyticsSDK.Net.EGAErrorSeverity.Error, "SetTransformIgnoreContacts threw an exception in SetContainedItemPositions (" + e.Message + ")\n" + e.StackTrace); } + contained.body.Submarine = item.Submarine; } contained.Rect = diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index ab5aa5c90..4a69f5499 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -39,8 +39,6 @@ namespace Barotrauma.Items.Components private Item focusTarget; private float targetRotation; - private bool state; - public Vector2 UserPos { get { return userPos; } @@ -61,6 +59,18 @@ namespace Barotrauma.Items.Components set; } + [Editable, Serialize(false, false, description: "Whether the item is toggled on/off. Only valid if IsToggle is set to true.")] + public bool State + { + get; + set; + } + + public bool ControlCharacterPose + { + get { return limbPositions.Count > 0; } + } + public Controller(Item item, XElement element) : base(item, element) { @@ -99,7 +109,7 @@ namespace Barotrauma.Items.Components if (IsToggle) { - item.SendSignal(0, state ? "1" : "0", "signal_out", sender: null); + item.SendSignal(0, State ? "1" : "0", "signal_out", sender: null); } if (user == null @@ -272,7 +282,7 @@ namespace Barotrauma.Items.Components return true; } - private Item GetFocusTarget() + public Item GetFocusTarget() { item.SendSignal(0, MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture), "position_out", user); @@ -294,7 +304,7 @@ namespace Barotrauma.Items.Components { if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { - state = !state; + State = !State; #if SERVER item.CreateServerEvent(this); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index 424665d8a..64c1233eb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -113,7 +113,7 @@ namespace Barotrauma.Items.Components if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { - if (targetItem.Prefab.DeconstructItems.Any()) + if (targetItem.Prefab.AllowDeconstruct) { //drop all items that are inside the deconstructed item foreach (ItemContainer ic in targetItem.GetComponents()) @@ -200,7 +200,7 @@ namespace Barotrauma.Items.Components #if SERVER if (user != null) { - GameServer.Log(user.LogName + (IsActive ? " activated " : " deactivated ") + item.Name, ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(user) + (IsActive ? " activated " : " deactivated ") + item.Name, ServerLog.MessageType.ItemInteraction); } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs index df57597b1..540c6ec98 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs @@ -23,7 +23,7 @@ namespace Barotrauma.Items.Components private float prevVoltage; private float controlLockTimer; - + [Editable(0.0f, 10000000.0f), Serialize(2000.0f, true, description: "The amount of force exerted on the submarine when the engine is operating at full power.")] public float MaxForce @@ -119,12 +119,15 @@ namespace Barotrauma.Items.Components float max = 1 + maxChangeSpeed; UpdateAITargets(Math.Clamp(noise, min, max), deltaTime); #if CLIENT - for (int i = 0; i < 5; i++) + particleTimer -= deltaTime; + if (particleTimer <= 0.0f) { + Vector2 particleVel = -currForce.ClampLength(5000.0f) / 5.0f; GameMain.ParticleManager.CreateParticle("bubbles", item.WorldPosition + PropellerPos, - -currForce / 5.0f + new Vector2(Rand.Range(-100.0f, 100.0f), Rand.Range(-50f, 50f)), + particleVel * Rand.Range(0.9f, 1.1f), 0.0f, item.CurrentHull); - } + particleTimer = 1.0f / particlesPerSec; + } #endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 89cb750fc..746bca34a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -180,7 +180,7 @@ namespace Barotrauma.Items.Components #if SERVER if (user != null) { - GameServer.Log(user.LogName + " started fabricating " + selectedItem.DisplayName + " in " + item.Name, ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(user) + " started fabricating " + selectedItem.DisplayName + " in " + item.Name, ServerLog.MessageType.ItemInteraction); } #endif } @@ -216,7 +216,7 @@ namespace Barotrauma.Items.Components #if SERVER if (user != null) { - GameServer.Log(user.LogName + " cancelled the fabrication of " + fabricatedItem.DisplayName + " in " + item.Name, ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(user) + " cancelled the fabrication of " + fabricatedItem.DisplayName + " in " + item.Name, ServerLog.MessageType.ItemInteraction); } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs index 49c294f4a..b9f61f395 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/OxygenGenerator.cs @@ -22,7 +22,7 @@ namespace Barotrauma.Items.Components private set; } - [Editable, Serialize(400.0f, true, description: "How much oxygen the machine generates when operating at full power.")] + [Editable, Serialize(400.0f, true, description: "How much oxygen the machine generates when operating at full power.", alwaysUseInstanceValues: true)] public float GeneratedAmount { get { return generatedAmount; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index fa2c78107..de6db2cef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -27,7 +27,7 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(80.0f, false, description: "How fast the item pumps water in/out when operating at 100%.")] + [Editable, Serialize(80.0f, false, description: "How fast the item pumps water in/out when operating at 100%.", alwaysUseInstanceValues: true)] public float MaxFlow { get { return maxFlow; } @@ -45,6 +45,7 @@ namespace Barotrauma.Items.Components } public bool HasPower => IsActive && Voltage >= MinVoltage; + public bool IsAutoControlled => pumpSpeedLockTimer > 0.0f || isActiveLockTimer > 0.0f; public Pump(Item item, XElement element) : base(item, element) @@ -130,7 +131,7 @@ namespace Barotrauma.Items.Components if (objective.Option.Equals("stoppumping", StringComparison.OrdinalIgnoreCase)) { #if SERVER - if (FlowPercentage > 0.0f) + if (objective.Override || FlowPercentage > 0.0f) { item.CreateServerEvent(this); } @@ -141,7 +142,7 @@ namespace Barotrauma.Items.Components else { #if SERVER - if (!IsActive || FlowPercentage > -100.0f) + if (objective.Override || !IsActive || FlowPercentage > -100.0f) { item.CreateServerEvent(this); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs index 82f3f24f6..725058ca2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs @@ -78,7 +78,7 @@ namespace Barotrauma.Items.Components } } - [Editable(0.0f, float.MaxValue), Serialize(10000.0f, true, description: "How much power (kW) the reactor generates when operating at full capacity.")] + [Editable(0.0f, float.MaxValue), Serialize(10000.0f, true, description: "How much power (kW) the reactor generates when operating at full capacity.", alwaysUseInstanceValues: true)] public float MaxPowerOutput { get { return maxPowerOutput; } @@ -190,7 +190,7 @@ namespace Barotrauma.Items.Components { if (Timing.TotalTime >= (float)nextServerLogWriteTime) { - GameServer.Log(lastUser.LogName + " adjusted reactor settings: " + + GameServer.Log(GameServer.CharacterLogName(lastUser) + " adjusted reactor settings: " + "Temperature: " + (int)(temperature * 100.0f) + ", Fission rate: " + (int)targetFissionRate + ", Turbine output: " + (int)targetTurbineOutput + @@ -330,6 +330,8 @@ namespace Barotrauma.Items.Components } item.SendSignal(0, ((int)(temperature * 100.0f)).ToString(), "temperature_out", null); + item.SendSignal(0, ((int)-CurrPowerConsumption).ToString(), "power_value_out", null); + item.SendSignal(0, ((int)load).ToString(), "load_value_out", null); UpdateFailures(deltaTime); #if CLIENT @@ -622,7 +624,8 @@ namespace Barotrauma.Items.Components AutoTemp = false; targetFissionRate = 0.0f; targetTurbineOutput = 0.0f; - break; + unsentChanges = true; + return true; } if (autoTemp != prevAutoTemp || @@ -653,17 +656,23 @@ namespace Barotrauma.Items.Components } break; case "set_fissionrate": - if (float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float newFissionRate)) + if (PowerOn && float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float newFissionRate)) { - FissionRate = newFissionRate; + targetFissionRate = newFissionRate; unsentChanges = true; +#if CLIENT + FissionRateScrollBar.BarScroll = targetFissionRate / 100.0f; +#endif } break; case "set_turbineoutput": - if (float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float newTurbineOutput)) + if (PowerOn && float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float newTurbineOutput)) { - TurbineOutput = newTurbineOutput; + targetTurbineOutput = newTurbineOutput; unsentChanges = true; +#if CLIENT + TurbineOutputScrollBar.BarScroll = targetTurbineOutput / 100.0f; +#endif } break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index 2fb664778..0f382768f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -108,10 +108,17 @@ namespace Barotrauma.Items.Components public Vector2 TargetVelocity { - get { return targetVelocity;} - set + get { return targetVelocity; } + set { - if (!MathUtils.IsValid(value)) return; + if (!MathUtils.IsValid(value)) + { + if (!MathUtils.IsValid(targetVelocity)) + { + targetVelocity = Vector2.Zero; + } + return; + } targetVelocity.X = MathHelper.Clamp(value.X, -100.0f, 100.0f); targetVelocity.Y = MathHelper.Clamp(value.Y, -100.0f, 100.0f); } @@ -285,7 +292,7 @@ namespace Barotrauma.Items.Components if (AutoPilot) { UpdateAutoPilot(deltaTime); - targetVelocity = targetVelocity.ClampLength(MathHelper.Lerp(AutoPilotMaxSpeed, AIPilotMaxSpeed, userSkill) * 100.0f); + TargetVelocity = TargetVelocity.ClampLength(MathHelper.Lerp(AutoPilotMaxSpeed, AIPilotMaxSpeed, userSkill) * 100.0f); } else { @@ -443,41 +450,46 @@ namespace Barotrauma.Items.Components //steer away from other subs foreach (Submarine sub in Submarine.Loaded) { - if (sub == controlledSub) continue; - if (controlledSub.DockedTo.Contains(sub)) continue; - - float thisSize = Math.Max(controlledSub.Borders.Width, controlledSub.Borders.Height); - float otherSize = Math.Max(sub.Borders.Width, sub.Borders.Height); - + if (sub == controlledSub) { continue; } + if (controlledSub.DockedTo.Contains(sub)) { continue; } + Point sizeSum = controlledSub.Borders.Size + sub.Borders.Size; + Vector2 minDist = sizeSum.ToVector2() / 2; Vector2 diff = controlledSub.WorldPosition - sub.WorldPosition; - float dist = diff == Vector2.Zero ? 0.0f : diff.Length(); - - //far enough -> ignore - if (dist > thisSize + otherSize) continue; - - Vector2 dir = dist <= 0.0001f ? Vector2.UnitY : diff / dist; - float dot = controlledSub.Velocity == Vector2.Zero ? - 0.0f : Vector2.Dot(Vector2.Normalize(controlledSub.Velocity), -dir); - - //heading away -> ignore - if (dot < 0.0f) continue; - - targetVelocity += diff * 200.0f; + float xDist = Math.Abs(diff.X); + float yDist = Math.Abs(diff.Y); + Vector2 maxAvoidDistance = minDist * 2; + if (xDist > maxAvoidDistance.X || yDist > maxAvoidDistance.Y) + { + //far enough -> ignore + continue; + } + float dot = controlledSub.Velocity == Vector2.Zero ? 0.0f : Vector2.Dot(Vector2.Normalize(controlledSub.Velocity), -diff); + if (dot < 0.0f) + { + //heading away -> ignore + continue; + } + float distanceFactor = MathHelper.Lerp(0, 1, MathUtils.InverseLerp(maxAvoidDistance.X + maxAvoidDistance.Y, minDist.X + minDist.Y, xDist + yDist)); + float velocityFactor = MathHelper.Lerp(0, 1, MathUtils.InverseLerp(0, 3, controlledSub.Velocity.Length())); + TargetVelocity += 100 * Vector2.Normalize(diff) * distanceFactor * velocityFactor; } - //clamp velocity magnitude to 100.0f - float velMagnitude = targetVelocity.Length(); + //clamp velocity magnitude to 100.0f (Is this required? The X and Y components are clamped in the property setter) + float velMagnitude = TargetVelocity.Length(); if (velMagnitude > 100.0f) { - targetVelocity *= 100.0f / velMagnitude; + TargetVelocity *= 100.0f / velMagnitude; } } private void UpdatePath() { if (Level.Loaded == null) { return; } - - if (pathFinder == null) pathFinder = new PathFinder(WayPoint.WayPointList, false); + + if (pathFinder == null) + { + pathFinder = new PathFinder(WayPoint.WayPointList, false); + } Vector2 target; if (LevelEndSelected) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs index e136155a2..a01f75343 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs @@ -316,6 +316,8 @@ namespace Barotrauma.Items.Components { if (recipient.Item == item || recipient.Item == source) { continue; } + source?.LastSentSignalRecipients.Add(recipient.Item); + foreach (ItemComponent ic in recipient.Item.Components) { //other junction boxes don't need to receive the signal in the pass-through signal connections diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs index 1049831b0..319a44aa6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs @@ -140,6 +140,7 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { + currPowerConsumption = powerConsumption; UpdateOnActiveEffects(deltaTime); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index cdfde4231..81b272c2c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -241,7 +241,6 @@ namespace Barotrauma.Items.Components private void Launch(Vector2 impulse) { hits.Clear(); - MaxTargetsToHit = 2; if (item.AiTarget != null) { @@ -294,6 +293,12 @@ namespace Barotrauma.Items.Components { //shooting indoors, do a hitscan outside as well hits.AddRange(DoRayCast(rayStart + item.Submarine.SimPosition, rayEnd + item.Submarine.SimPosition)); + //also in the coordinate space of docked subs + foreach (Submarine dockedSub in item.Submarine.DockedTo) + { + if (dockedSub == item.Submarine) { continue; } + hits.AddRange(DoRayCast(rayStart + item.Submarine.SimPosition - dockedSub.SimPosition, rayEnd + item.Submarine.SimPosition - dockedSub.SimPosition)); + } } else { @@ -354,7 +359,7 @@ namespace Barotrauma.Items.Components { //ignore sensors and items if (fixture?.Body == null || fixture.IsSensor) { return true; } - if (fixture.Body.UserData is Item item && item.GetComponent() == null && !item.Prefab.DamagedByProjectiles) { return true; } + if (fixture.Body.UserData is Item item && (item.GetComponent() == null && !item.Prefab.DamagedByProjectiles || item.Condition <= 0)) { return true; } if (fixture.Body?.UserData as string == "ruinroom") { return true; } //ignore everything else than characters, sub walls and level walls @@ -374,7 +379,7 @@ namespace Barotrauma.Items.Components //ignore sensors and items if (fixture?.Body == null || fixture.IsSensor) { return -1; } - if (fixture.Body.UserData is Item item && item.GetComponent() == null && !item.Prefab.DamagedByProjectiles) { return -1; } + if (fixture.Body.UserData is Item item && (item.GetComponent() == null && !item.Prefab.DamagedByProjectiles || item.Condition <= 0)) { return -1; } if (fixture.Body?.UserData as string == "ruinroom") { return -1; } //ignore everything else than characters, sub walls and level walls @@ -410,9 +415,9 @@ namespace Barotrauma.Items.Components } } - if (stickJoint == null || StickPermanently) { return; } + if (stickJoint == null) { return; } - if (persistentStickJointTimer > 0.0f) + if (persistentStickJointTimer > 0.0f && !StickPermanently) { persistentStickJointTimer -= deltaTime; return; @@ -420,20 +425,25 @@ namespace Barotrauma.Items.Components if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { - if (stickJoint.JointTranslation < stickJoint.LowerLimit * 0.9f || - stickJoint.JointTranslation > stickJoint.UpperLimit * 0.9f) + if (StickTargetRemoved() || + (!StickPermanently && (stickJoint.JointTranslation < stickJoint.LowerLimit * 0.9f || stickJoint.JointTranslation > stickJoint.UpperLimit * 0.9f))) { Unstick(); - } #if SERVER - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) - { - item.CreateServerEvent(this); - } + item.CreateServerEvent(this); #endif + } } } + private bool StickTargetRemoved() + { + if (StickTarget == null) { return true; } + if (StickTarget.UserData is Limb limb) { return limb.character.Removed; } + if (StickTarget.UserData is Entity entity) { return entity.Removed; } + return false; + } + private bool OnProjectileCollision(Fixture f1, Fixture target, Contact contact) { @@ -529,7 +539,7 @@ namespace Barotrauma.Items.Components } else if (target.Body.UserData is Item targetItem) { - if (attack != null && targetItem.Prefab.DamagedByProjectiles) + if (attack != null && targetItem.Prefab.DamagedByProjectiles && targetItem.Condition > 0) { attackResult = attack.DoDamage(User, targetItem, item.WorldPosition, 1.0f); } @@ -604,7 +614,7 @@ namespace Barotrauma.Items.Components if (hits.Count() >= MaxTargetsToHit) { item.body.FarseerBody.OnCollision -= OnProjectileCollision; - if (item.Prefab.DamagedByProjectiles || item.Prefab.DamagedByMeleeWeapons) + if ((item.Prefab.DamagedByProjectiles || item.Prefab.DamagedByMeleeWeapons) && item.Condition > 0) { item.body.CollisionCategories = Physics.CollisionCharacter; item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform | Physics.CollisionProjectile; @@ -623,6 +633,7 @@ namespace Barotrauma.Items.Components item.body.LinearVelocity *= 0.1f; } else if (Vector2.Dot(velocity, collisionNormal) < 0.0f && hits.Count() >= MaxTargetsToHit && + target.Body.Mass > item.body.Mass * 0.5f && (DoesStick || (StickToCharacters && target.Body.UserData is Limb) || (StickToStructures && target.Body.UserData is Structure) || @@ -692,6 +703,7 @@ namespace Barotrauma.Items.Components if (StickPermanently) { stickJoint.LowerLimit = stickJoint.UpperLimit = 0.0f; + item.body.ResetDynamics(); } else if (item.Sprite != null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index 1b8e07500..674cd8874 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -273,7 +273,7 @@ namespace Barotrauma.Items.Components float successFactor = requiredSkills.Count == 0 ? 1.0f : DegreeOfSuccess(CurrentFixer, requiredSkills); //item must have been below the repair threshold for the player to get an achievement or XP for repairing it - if (!item.IsFullCondition) + if (item.ConditionPercentage < AIRepairThreshold) { wasBroken = true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs index 72b995824..87db1cef9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs @@ -120,6 +120,10 @@ namespace Barotrauma.Items.Components return; } +#if CLIENT + item.ResetCachedVisibleSize(); +#endif + if (SnapOnCollision) { raycastTimer += deltaTime; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs index 550fce815..4cfe3b515 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs @@ -13,7 +13,7 @@ namespace Barotrauma.Items.Components //the output is sent if both inputs have received a signal within the timeframe protected float timeFrame; - [InGameEditable(DecimalCount = 2), Serialize(0.0f, true, description: "The item sends the output if both inputs have received a non-zero signal within the timeframe. If set to 0, the inputs must receive a signal at the same time.")] + [InGameEditable(DecimalCount = 2), Serialize(0.0f, true, description: "The item sends the output if both inputs have received a non-zero signal within the timeframe. If set to 0, the inputs must receive a signal at the same time.", alwaysUseInstanceValues: true)] public float TimeFrame { get { return timeFrame; } @@ -23,14 +23,14 @@ namespace Barotrauma.Items.Components } } - [InGameEditable, Serialize("1", true, description: "The signal sent when both inputs have received a non-zero signal.")] + [InGameEditable, Serialize("1", true, description: "The signal sent when the condition is met.", alwaysUseInstanceValues: true)] public string Output { get { return output; } set { output = value; } } - [InGameEditable, Serialize("", true, description: "The signal sent when both inputs have not received a non-zero signal (if empty, no signal is sent).")] + [InGameEditable, Serialize("", true, description: "The signal sent when the condition is met (if empty, no signal is sent).", alwaysUseInstanceValues: true)] public string FalseOutput { get { return falseOutput; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs index f0f042a40..b1c4a63b2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ArithmeticComponent.cs @@ -15,7 +15,7 @@ namespace Barotrauma.Items.Components //the output is sent if both inputs have received a signal within the timeframe protected float timeFrame; - [Serialize(999999.0f, true, description: "The output of the item is restricted below this value."), + [Serialize(999999.0f, true, description: "The output of the item is restricted below this value.", alwaysUseInstanceValues: true), InGameEditable(MinValueFloat = -999999.0f, MaxValueFloat = 999999.0f)] public float ClampMax { @@ -23,7 +23,7 @@ namespace Barotrauma.Items.Components set; } - [Serialize(-999999.0f, true, description: "The output of the item is restricted above this value."), + [Serialize(-999999.0f, true, description: "The output of the item is restricted above this value.", alwaysUseInstanceValues: true), InGameEditable(MinValueFloat = -999999.0f, MaxValueFloat = 999999.0f)] public float ClampMin { @@ -32,8 +32,8 @@ namespace Barotrauma.Items.Components } [InGameEditable(DecimalCount = 2), - Serialize(0.0f, true, description: "The item must have received signals to both inputs within this timeframe to output the sum of the signals." + - " If set to 0, the inputs must be received at the same time.")] + Serialize(0.0f, true, description: "The item must have received signals to both inputs within this timeframe to output the result." + + " If set to 0, the inputs must be received at the same time.", alwaysUseInstanceValues: true)] public float TimeFrame { get { return timeFrame; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs index e73698405..5881d2a13 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs @@ -21,7 +21,7 @@ namespace Barotrauma.Items.Components private List disconnectedWireIds; - [Editable, Serialize(false, true, description: "Locked connection panels cannot be rewired in-game.")] + [Editable, Serialize(false, true, description: "Locked connection panels cannot be rewired in-game.", alwaysUseInstanceValues: true)] public bool Locked { get; @@ -192,6 +192,8 @@ namespace Barotrauma.Items.Components public bool CheckCharacterSuccess(Character character) { if (character == null) { return false; } + //no electrocution in sub editor + if (Screen.Selected == GameMain.SubEditorScreen) { return true; } var powered = item.GetComponent(); if (powered != null) @@ -262,7 +264,18 @@ namespace Barotrauma.Items.Components { if (wire.OtherConnection(null) == null) //wire not connected to anything else { +#if CLIENT + if (SubEditorScreen.IsSubEditor()) + { + wire.Item.Remove(); + } + else + { + wire.Item.Drop(null); + } +#else wire.Item.Drop(null); +#endif } } @@ -275,7 +288,18 @@ namespace Barotrauma.Items.Components if (wire.OtherConnection(c) == null) //wire not connected to anything else { +#if CLIENT + if (SubEditorScreen.IsSubEditor()) + { + wire.Item.Remove(); + } + else + { + wire.Item.Drop(null); + } +#else wire.Item.Drop(null); +#endif } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs index 1078353f8..63842df7d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs @@ -30,7 +30,7 @@ namespace Barotrauma.Items.Components private DelayedSignal prevQueuedSignal; private float delay; - [InGameEditable(MinValueFloat = 0.0f, MaxValueFloat = 60.0f, DecimalCount = 2), Serialize(1.0f, true, description: "How long the item delays the signals (in seconds).")] + [InGameEditable(MinValueFloat = 0.0f, MaxValueFloat = 60.0f, DecimalCount = 2), Serialize(1.0f, true, description: "How long the item delays the signals (in seconds).", alwaysUseInstanceValues: true)] public float Delay { get { return delay; } @@ -43,14 +43,14 @@ namespace Barotrauma.Items.Components } } - [InGameEditable, Serialize(false, true, description: "Should the component discard previously received signals when a new one is received.")] + [InGameEditable, Serialize(false, true, description: "Should the component discard previously received signals when a new one is received.", alwaysUseInstanceValues: true)] public bool ResetWhenSignalReceived { get; set; } - [InGameEditable, Serialize(false, true, description: "Should the component discard previously received signals when the incoming signal changes.")] + [InGameEditable, Serialize(false, true, description: "Should the component discard previously received signals when the incoming signal changes.", alwaysUseInstanceValues: true)] public bool ResetWhenDifferentSignalReceived { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs index 467418095..6ba37f496 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs @@ -15,21 +15,21 @@ namespace Barotrauma.Items.Components //the output is sent if both inputs have received a signal within the timeframe protected float timeFrame; - [InGameEditable, Serialize("1", true, description: "The signal this item outputs when the received signals are equal.")] + [InGameEditable, Serialize("1", true, description: "The signal this item outputs when the condition is met.", alwaysUseInstanceValues: true)] public string Output { get { return output; } set { output = value; } } - [InGameEditable, Serialize("", true, description: "The signal this item outputs when the received signals are not equal.")] + [InGameEditable, Serialize("", true, description: "The signal this item outputs when the condition is not met.", alwaysUseInstanceValues: true)] public string FalseOutput { get { return falseOutput; } set { falseOutput = value; } } - [InGameEditable(DecimalCount = 2), Serialize(0.0f, true, description: "The maximum amount of time between the received signals. If set to 0, the signals must be received at the same time.")] + [InGameEditable(DecimalCount = 2), Serialize(0.0f, true, description: "The maximum amount of time between the received signals. If set to 0, the signals must be received at the same time.", alwaysUseInstanceValues: true)] public float TimeFrame { get { return timeFrame; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ExponentiationComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ExponentiationComponent.cs index 59b81abce..4d8fba217 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ExponentiationComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ExponentiationComponent.cs @@ -6,7 +6,7 @@ namespace Barotrauma.Items.Components class ExponentiationComponent : ItemComponent { private float exponent; - [InGameEditable, Serialize(1.0f, false, description: "The exponent of the operation.")] + [InGameEditable, Serialize(1.0f, false, description: "The exponent of the operation.", alwaysUseInstanceValues: true)] public float Exponent { get diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/FunctionComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/FunctionComponent.cs index a34a96c7b..2c529781e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/FunctionComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/FunctionComponent.cs @@ -16,7 +16,7 @@ namespace Barotrauma.Items.Components SquareRoot } - [Serialize(FunctionType.Round, false, description: "Which kind of function to run the input through.")] + [Serialize(FunctionType.Round, false, description: "Which kind of function to run the input through.", alwaysUseInstanceValues: true)] public FunctionType Function { get; set; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index 593b7113f..4f92e27b1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -15,7 +15,7 @@ namespace Barotrauma.Items.Components private float lightBrightness; private float blinkFrequency; private float range; - private float flicker; + private float flicker, flickerState; private bool castShadows; private bool drawBehindSubs; @@ -25,7 +25,7 @@ namespace Barotrauma.Items.Components public PhysicsBody ParentBody; - [Serialize(100.0f, true, description: "The range of the emitted light. Higher values are more performance-intensive."), + [Serialize(100.0f, true, description: "The range of the emitted light. Higher values are more performance-intensive.", alwaysUseInstanceValues: true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2048.0f)] public float Range { @@ -34,6 +34,7 @@ namespace Barotrauma.Items.Components { range = MathHelper.Clamp(value, 0.0f, 4096.0f); #if CLIENT + item.ResetCachedVisibleSize(); if (light != null) { light.Range = range; } #endif } @@ -42,7 +43,7 @@ namespace Barotrauma.Items.Components public float Rotation; [Editable, Serialize(true, true, description: "Should structures cast shadows when light from this light source hits them. " + - "Disabling shadows increases the performance of the game, and is recommended for lights with a short range.")] + "Disabling shadows increases the performance of the game, and is recommended for lights with a short range.", alwaysUseInstanceValues: true)] public bool CastShadows { get { return castShadows; } @@ -56,7 +57,7 @@ namespace Barotrauma.Items.Components } [Editable, Serialize(false, true, description: "Lights drawn behind submarines don't cast any shadows and are much faster to draw than shadow-casting lights. " + - "It's recommended to enable this on decorative lights outside the submarine's hull.")] + "It's recommended to enable this on decorative lights outside the submarine's hull.", alwaysUseInstanceValues: true)] public bool DrawBehindSubs { get { return drawBehindSubs; } @@ -69,7 +70,7 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(false, true, description: "Is the light currently on.")] + [Editable, Serialize(false, true, description: "Is the light currently on.", alwaysUseInstanceValues: true)] public bool IsOn { get { return IsActive; } @@ -82,7 +83,7 @@ namespace Barotrauma.Items.Components } } - [Serialize(0.0f, false, description: "How heavily the light flickers. 0 = no flickering, 1 = the light will alternate between completely dark and full brightness.")] + [Editable, Serialize(0.0f, false, description: "How heavily the light flickers. 0 = no flickering, 1 = the light will alternate between completely dark and full brightness.")] public float Flicker { get { return flicker; } @@ -92,6 +93,13 @@ namespace Barotrauma.Items.Components } } + [Editable, Serialize(1.0f, false, description: "How fast the light flickers.")] + public float FlickerSpeed + { + get; + set; + } + [Editable, Serialize(0.0f, true, description: "How rapidly the light blinks on and off (in Hz). 0 = no blinking.")] public float BlinkFrequency { @@ -102,7 +110,7 @@ namespace Barotrauma.Items.Components } } - [InGameEditable, Serialize("255,255,255,255", true, description: "The color of the emitted light (R,G,B,A).")] + [InGameEditable, Serialize("255,255,255,255", true, description: "The color of the emitted light (R,G,B,A).", alwaysUseInstanceValues: true)] public Color LightColor { get { return lightColor; } @@ -217,7 +225,7 @@ namespace Barotrauma.Items.Components } else { - lightBrightness = MathHelper.Lerp(lightBrightness, Math.Min(Voltage, 1.0f), 0.1f); + lightBrightness = MathHelper.Lerp(lightBrightness, powerConsumption <= 0.0f ? 1.0f : Math.Min(Voltage, 1.0f), 0.1f); } if (blinkFrequency > 0.0f) @@ -231,7 +239,10 @@ namespace Barotrauma.Items.Components } else { - SetLightSourceState(true, lightBrightness * (1.0f - Rand.Range(0.0f, flicker))); + flickerState += deltaTime * FlickerSpeed; + flickerState %= 255; + float noise = PerlinNoise.GetPerlin(flickerState, flickerState * 0.5f) * flicker; + SetLightSourceState(true, lightBrightness * (1.0f - noise)); } if (powerIn == null && powerConsumption > 0.0f) { Voltage -= deltaTime; } @@ -254,7 +265,7 @@ namespace Barotrauma.Items.Components switch (connection.Name) { case "toggle": - if (IgnoreContinuousToggle && lastToggleSignalTime < Timing.TotalTime - 0.1) + if (!IgnoreContinuousToggle || lastToggleSignalTime < Timing.TotalTime - 0.1) { IsOn = !IsOn; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs index ebfd0b2c4..0fd1c5a51 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MemoryComponent.cs @@ -4,7 +4,7 @@ namespace Barotrauma.Items.Components { class MemoryComponent : ItemComponent { - [InGameEditable, Serialize("", true, description: "The currently stored signal the item outputs.")] + [InGameEditable, Serialize("", true, description: "The currently stored signal the item outputs.", alwaysUseInstanceValues: true)] public string Value { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ModuloComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ModuloComponent.cs index d4e926de0..a66276988 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ModuloComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ModuloComponent.cs @@ -6,7 +6,7 @@ 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.")] + [InGameEditable, Serialize(1.0f, false, description: "The modulus of the operation. Must be non-zero.", alwaysUseInstanceValues: true)] public float Modulus { get { return modulus; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs index a672b8e63..9f10c0973 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs @@ -18,14 +18,14 @@ namespace Barotrauma.Items.Components [Serialize(false, false, description: "Has the item currently detected movement. Intended to be used by StatusEffect conditionals (setting this value in XML has no effect).")] public bool MotionDetected { get; set; } - [Editable, Serialize(false, true, description: "Should the sensor only detect the movement of humans?")] + [Editable, Serialize(false, true, description: "Should the sensor only detect the movement of humans?", alwaysUseInstanceValues: true)] public bool OnlyHumans { get; set; } - [Editable, Serialize(false, true, description: "Should the sensor ignore the bodies of dead characters?")] + [Editable, Serialize(false, true, description: "Should the sensor ignore the bodies of dead characters?", alwaysUseInstanceValues: true)] public bool IgnoreDead { get; @@ -33,16 +33,19 @@ namespace Barotrauma.Items.Components } - [InGameEditable, Serialize(0.0f, true, description: "Horizontal detection range.")] + [InGameEditable, Serialize(0.0f, true, description: "Horizontal detection range.", alwaysUseInstanceValues: true)] public float RangeX { get { return rangeX; } set { rangeX = MathHelper.Clamp(value, 0.0f, 1000.0f); +#if CLIENT + item.ResetCachedVisibleSize(); +#endif } } - [InGameEditable, Serialize(0.0f, true, description: "Vertical movement detection range.")] + [InGameEditable, Serialize(0.0f, true, description: "Vertical movement detection range.", alwaysUseInstanceValues: true)] public float RangeY { get { return rangeY; } @@ -64,13 +67,13 @@ namespace Barotrauma.Items.Components } } - [InGameEditable, Serialize("1", true, description: "The signal the item outputs when it has detected movement.")] + [InGameEditable, Serialize("1", true, description: "The signal the item outputs when it has detected movement.", alwaysUseInstanceValues: true)] public string Output { get; set; } - [InGameEditable, Serialize("", true, description: "The signal the item outputs when it has not detected movement.")] + [InGameEditable, Serialize("", true, description: "The signal the item outputs when it has not detected movement.", alwaysUseInstanceValues: true)] public string FalseOutput { get; set; } - [Editable(DecimalCount = 3), Serialize(0.01f, true, description: "How fast the objects within the detector's range have to be moving (in m/s).")] + [Editable(DecimalCount = 3), Serialize(0.01f, true, description: "How fast the objects within the detector's range have to be moving (in m/s).", alwaysUseInstanceValues: true)] public float MinimumVelocity { get; @@ -129,7 +132,8 @@ namespace Barotrauma.Items.Components foreach (Limb limb in c.AnimController.Limbs) { - if (limb.LinearVelocity.LengthSquared() <= MinimumVelocity * MinimumVelocity) continue; + if (limb.IsSevered) { continue; } + if (limb.LinearVelocity.LengthSquared() <= MinimumVelocity * MinimumVelocity) { continue; } if (MathUtils.CircleIntersectsRectangle(limb.WorldPosition, ConvertUnits.ToDisplayUnits(limb.body.GetMaxExtent()), detectRect)) { MotionDetected = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OscillatorComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OscillatorComponent.cs index 3b2b7ece7..caacada54 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OscillatorComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OscillatorComponent.cs @@ -23,14 +23,14 @@ namespace Barotrauma.Items.Components [InGameEditable, Serialize(WaveType.Pulse, true, description: "What kind of a signal the item outputs." + " Pulse: periodically sends out a signal of 1." + " Sine: sends out a sine wave oscillating between -1 and 1." + - " Square: sends out a signal that alternates between 0 and 1.")] + " Square: sends out a signal that alternates between 0 and 1.", alwaysUseInstanceValues: true)] public WaveType OutputType { get; set; } - [InGameEditable(DecimalCount = 2), Serialize(1.0f, true, description: "How fast the signal oscillates, or how fast the pulses are sent (in Hz).")] + [InGameEditable(DecimalCount = 2), Serialize(1.0f, true, description: "How fast the signal oscillates, or how fast the pulses are sent (in Hz).", alwaysUseInstanceValues: true)] public float Frequency { get { return frequency; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs index e40ca77ed..a541bc98f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RegExFindComponent.cs @@ -16,16 +16,16 @@ namespace Barotrauma.Items.Components private bool nonContinuousOutputSent; - [InGameEditable, Serialize("1", true, description: "The signal this item outputs when the received signal matches the regular expression.")] + [InGameEditable, Serialize("1", true, description: "The signal this item outputs when the received signal matches the regular expression.", alwaysUseInstanceValues: true)] public string Output { get; set; } - [Serialize("0", true, description: "The signal this item outputs when the received signal does not match the regular expression.")] + [Serialize("0", true, description: "The signal this item outputs when the received signal does not match the regular expression.", alwaysUseInstanceValues: true)] public string FalseOutput { get; set; } - [InGameEditable, Serialize(true, true, description: "Should the component keep sending the output even after it stops receiving a signal, or only send an output when it receives a signal.")] + [InGameEditable, Serialize(true, true, description: "Should the component keep sending the output even after it stops receiving a signal, or only send an output when it receives a signal.", alwaysUseInstanceValues: true)] public bool ContinuousOutput { get; set; } - [InGameEditable, Serialize("", true, description: "The regular expression used to check the incoming signals.")] + [InGameEditable, Serialize("", true, description: "The regular expression used to check the incoming signals.", alwaysUseInstanceValues: true)] public string Expression { get { return expression; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs index 8061c0789..7f5019107 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/RelayComponent.cs @@ -37,7 +37,7 @@ namespace Barotrauma.Items.Components } } - [Editable, Serialize(false, true, description: "Can the relay currently pass power and signals through it.")] + [Editable, Serialize(false, true, description: "Can the relay currently pass power and signals through it.", alwaysUseInstanceValues: true)] public bool IsOn { get diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SignalCheckComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SignalCheckComponent.cs index 13e2acc3c..eb725383d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SignalCheckComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SignalCheckComponent.cs @@ -4,12 +4,12 @@ namespace Barotrauma.Items.Components { class SignalCheckComponent : ItemComponent { - [InGameEditable, Serialize("1", true, description: "The signal this item outputs when the received signal matches the target signal.")] + [InGameEditable, Serialize("1", true, description: "The signal this item outputs when the received signal matches the target signal.", alwaysUseInstanceValues: true)] public string Output { get; set; } - [InGameEditable, Serialize("0", true, description: "The signal this item outputs when the received signal does not match the target signal.")] + [InGameEditable, Serialize("0", true, description: "The signal this item outputs when the received signal does not match the target signal.", alwaysUseInstanceValues: true)] public string FalseOutput { get; set; } - [InGameEditable, Serialize("", true, description: "The value to compare the received signals against.")] + [InGameEditable, Serialize("", true, description: "The value to compare the received signals against.", alwaysUseInstanceValues: true)] public string TargetSignal { get; set; } public SignalCheckComponent(Item item, XElement element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SmokeDetector.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SmokeDetector.cs index feed5a033..7a85ef85f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SmokeDetector.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/SmokeDetector.cs @@ -1,25 +1,51 @@ -using System.Linq; -using System.Xml.Linq; +using System.Xml.Linq; namespace Barotrauma.Items.Components { class SmokeDetector : ItemComponent { - [Serialize(50.0f, false, description: "How large the fire has to be for the detector to react to it.")] - public float FireSizeThreshold - { - get; set; - } + const float FireCheckInterval = 1.0f; + private float fireCheckTimer; + + private bool fireInRange; + + [InGameEditable, Serialize("1", true, description: "The signal the item outputs when it has detected movement.", alwaysUseInstanceValues: true)] + public string Output { get; set; } + + [InGameEditable, Serialize("0", true, description: "The signal the item outputs when it has not detected movement.", alwaysUseInstanceValues: true)] + public string FalseOutput { get; set; } public SmokeDetector(Item item, XElement element) - : base (item, element) + : base(item, element) { IsActive = true; } + private bool IsFireInRange() + { + if (item.CurrentHull == null || item.InWater) { return false; } + + var connectedHulls = item.CurrentHull.GetConnectedHulls(includingThis: true, searchDepth: 10, ignoreClosedGaps: true); + foreach (Hull hull in connectedHulls) + { + foreach (FireSource fireSource in hull.FireSources) + { + if (fireSource.IsInDamageRange(item.WorldPosition, fireSource.DamageRange * 2.0f)) { return true; } + } + } + + return false; + } + public override void Update(float deltaTime, Camera cam) { - item.SendSignal(0, item.CurrentHull != null && item.CurrentHull.FireSources.Any(fs => fs.Size.X > FireSizeThreshold) ? "1" : "0", "signal_out", null); + fireCheckTimer -= deltaTime; + if (fireCheckTimer <= 0.0f) + { + fireInRange = IsFireInRange(); + fireCheckTimer = FireCheckInterval; + } + item.SendSignal(0, fireInRange ? "1" : "0", "signal_out", null); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs index dcad68832..dad4b40fa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Terminal.cs @@ -13,7 +13,7 @@ namespace Barotrauma.Items.Components } private string welcomeMessage; - [InGameEditable, Serialize("", true, "Message to be displayed on the terminal display when it is first opened.", translationTextTag = "terminalwelcomemsg.")] + [InGameEditable, Serialize("", true, "Message to be displayed on the terminal display when it is first opened.", translationTextTag = "terminalwelcomemsg.", AlwaysUseInstanceValues = true)] public string WelcomeMessage { get { return welcomeMessage; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs index 14d183268..640a7f624 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/TrigonometricFunctionComponent.cs @@ -19,14 +19,14 @@ namespace Barotrauma.Items.Components protected float[] receivedSignal = new float[2]; - [Serialize(FunctionType.Sin, false, description: "Which kind of function to run the input through.")] + [Serialize(FunctionType.Sin, false, description: "Which kind of function to run the input through.", alwaysUseInstanceValues: true)] public FunctionType Function { get; set; } - [InGameEditable, Serialize(false, true, description: "If set to true, the trigonometric function uses radians instead of degrees.")] + [InGameEditable, Serialize(false, true, description: "If set to true, the trigonometric function uses radians instead of degrees.", alwaysUseInstanceValues: true)] public bool UseRadians { get; set; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs index d5ad89235..830441855 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs @@ -10,10 +10,10 @@ namespace Barotrauma.Items.Components private bool isInWater; private float stateSwitchDelay; - [InGameEditable, Serialize("1", true, description: "The signal the item sends out when it's underwater.")] + [InGameEditable, Serialize("1", true, description: "The signal the item sends out when it's underwater.", alwaysUseInstanceValues: true)] public string Output { get; set; } - [InGameEditable, Serialize("0", true, description: "The signal the item sends out when it's not underwater.")] + [InGameEditable, Serialize("0", true, description: "The signal the item sends out when it's not underwater.", alwaysUseInstanceValues: true)] public string FalseOutput { get; set; } public WaterDetector(Item item, XElement element) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs index 5513e315e..93b88b2f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs @@ -9,7 +9,7 @@ namespace Barotrauma.Items.Components { partial class WifiComponent : ItemComponent { - private static List list = new List(); + private static readonly List list = new List(); private float range; @@ -19,17 +19,23 @@ namespace Barotrauma.Items.Components private string prevSignal; - [Serialize(Character.TeamType.None, true, description: "WiFi components can only communicate with components that have the same Team ID.")] + [Serialize(Character.TeamType.None, true, description: "WiFi components can only communicate with components that have the same Team ID.", alwaysUseInstanceValues: true)] public Character.TeamType TeamID { get; set; } - [Editable, 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.", alwaysUseInstanceValues: true)] public float Range { get { return range; } - set { range = Math.Max(value, 0.0f); } + set + { + range = Math.Max(value, 0.0f); +#if CLIENT + item.ResetCachedVisibleSize(); +#endif + } } - [InGameEditable, Serialize(1, true, description: "WiFi components can only communicate with components that use the same channel.")] + [InGameEditable, Serialize(1, true, description: "WiFi components can only communicate with components that use the same channel.", alwaysUseInstanceValues: true)] public int Channel { get { return channel; } @@ -40,7 +46,7 @@ namespace Barotrauma.Items.Components } - [Serialize(false, false, description: "Can the component communicate with wifi components in another team's submarine (e.g. enemy sub in Combat missions, respawn shuttle). Needs to be enabled on both the component transmitting the signal and the component receiving it.")] + [Serialize(false, false, description: "Can the component communicate with wifi components in another team's submarine (e.g. enemy sub in Combat missions, respawn shuttle). Needs to be enabled on both the component transmitting the signal and the component receiving it.", alwaysUseInstanceValues: true)] public bool AllowCrossTeamCommunication { get; @@ -48,7 +54,7 @@ namespace Barotrauma.Items.Components } [Editable, Serialize(false, false, description: "If enabled, any signals received from another chat-linked wifi component are displayed " + - "as chat messages in the chatbox of the player holding the item.")] + "as chat messages in the chatbox of the player holding the item.", alwaysUseInstanceValues: true)] public bool LinkToChat { get; @@ -105,12 +111,16 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { chatMsgCooldown -= deltaTime; + if (chatMsgCooldown <= 0.0f) + { + IsActive = false; + } } public void TransmitSignal(int stepsTaken, string signal, Item source, Character sender, bool sendToChat, float signalStrength = 1.0f) { var senderComponent = source?.GetComponent(); - if (senderComponent != null && !CanReceive(senderComponent)) return; + if (senderComponent != null && !CanReceive(senderComponent)) { return; } bool chatMsgSent = false; @@ -138,33 +148,32 @@ namespace Barotrauma.Items.Components if (LinkToChat && wifiComp.LinkToChat && chatMsgCooldown <= 0.0f && sendToChat) { if (wifiComp.item.ParentInventory != null && - wifiComp.item.ParentInventory.Owner != null && - GameMain.NetworkMember != null) + wifiComp.item.ParentInventory.Owner != null) { string chatMsg = signal; if (senderComponent != null) { chatMsg = ChatMessage.ApplyDistanceEffect(chatMsg, 1.0f - sentSignalStrength); } - if (chatMsg.Length > ChatMessage.MaxLength) chatMsg = chatMsg.Substring(0, ChatMessage.MaxLength); - if (string.IsNullOrEmpty(chatMsg)) continue; + if (chatMsg.Length > ChatMessage.MaxLength) { chatMsg = chatMsg.Substring(0, ChatMessage.MaxLength); } + if (string.IsNullOrEmpty(chatMsg)) { continue; } #if CLIENT if (wifiComp.item.ParentInventory.Owner == Character.Controlled) { if (GameMain.Client == null) - GameMain.NetworkMember.AddChatMessage(signal, ChatMessageType.Radio, source == null ? "" : source.Name); + { + GameMain.GameSession?.CrewManager?.AddSinglePlayerChatMessage(source?.Name ?? "", signal, ChatMessageType.Radio, sender: null); + } } -#endif - -#if SERVER +#elif SERVER if (GameMain.Server != null) { Client recipientClient = GameMain.Server.ConnectedClients.Find(c => c.Character == wifiComp.item.ParentInventory.Owner); if (recipientClient != null) { GameMain.Server.SendDirectChatMessage( - ChatMessage.Create(source == null ? "" : source.Name, chatMsg, ChatMessageType.Radio, null), recipientClient); + ChatMessage.Create(source?.Name ?? "", chatMsg, ChatMessageType.Radio, null), recipientClient); } } #endif @@ -172,15 +181,31 @@ namespace Barotrauma.Items.Components } } } - if (chatMsgSent) chatMsgCooldown = MinChatMessageInterval; + if (chatMsgSent) + { + chatMsgCooldown = MinChatMessageInterval; + IsActive = true; + } prevSignal = signal; } public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) { - if (connection == null || connection.Name != "signal_in") return; - TransmitSignal(stepsTaken, signal, source, sender, true, signalStrength); + if (connection == null) { return; } + + switch (connection.Name) + { + case "signal_in": + TransmitSignal(stepsTaken, signal, source, sender, true, signalStrength); + break; + case "set_channel": + if (int.TryParse(signal, out int newChannel)) + { + Channel = newChannel; + } + break; + } } protected override void RemoveComponentSpecific() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs index 94663b33e..fc0e86bb6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs @@ -5,6 +5,9 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Xml.Linq; +#if CLIENT +using Microsoft.Xna.Framework.Input; +#endif namespace Barotrauma.Items.Components { @@ -37,9 +40,11 @@ namespace Barotrauma.Items.Components } } + private bool shouldClearConnections = true; + const float MaxAttachDistance = 150.0f; - const float MinNodeDistance = 15.0f; + const float MinNodeDistance = 7.0f; const int MaxNodeCount = 255; const int MaxNodesPerNetworkEvent = 30; @@ -177,6 +182,8 @@ namespace Barotrauma.Items.Components newConnection.Item.Position : newConnection.Item.Position - refSub.HiddenSubPosition; + nodePos = RoundNode(nodePos); + if (nodes.Count > 0 && nodes[0] == nodePos) { break; } if (nodes.Count > 1 && nodes[nodes.Count - 1] == nodePos) { break; } @@ -241,7 +248,7 @@ namespace Barotrauma.Items.Components public override void Equip(Character character) { - ClearConnections(character); + if (shouldClearConnections) { ClearConnections(character); } IsActive = true; } @@ -253,7 +260,7 @@ namespace Barotrauma.Items.Components public override void Drop(Character dropper) { - ClearConnections(dropper); + if (shouldClearConnections) { ClearConnections(dropper); } IsActive = false; } @@ -334,7 +341,12 @@ namespace Barotrauma.Items.Components } else { +#if CLIENT + bool disableGrid = SubEditorScreen.IsSubEditor() && PlayerInput.IsShiftDown(); + newNodePos = disableGrid ? item.Position : RoundNode(item.Position); +#else newNodePos = RoundNode(item.Position); +#endif if (sub != null) { newNodePos -= sub.HiddenSubPosition; } canPlaceNode = true; } @@ -497,6 +509,9 @@ namespace Barotrauma.Items.Components sectionExtents.Y = Math.Max(Math.Abs(nodes[i].Y - item.Position.Y), sectionExtents.Y); } } +#if CLIENT + item.ResetCachedVisibleSize(); +#endif } public void ClearConnections(Character user = null) @@ -507,7 +522,7 @@ namespace Barotrauma.Items.Components foreach (Item item in Item.ItemList) { var connectionPanel = item.GetComponent(); - if (connectionPanel != null && connectionPanel.DisconnectedWires.Contains(this)) + if (connectionPanel != null && connectionPanel.DisconnectedWires.Contains(this) && !item.Removed) { #if SERVER item.CreateServerEvent(connectionPanel); @@ -526,18 +541,18 @@ namespace Barotrauma.Items.Components if (connections[0] != null && connections[1] != null) { - GameServer.Log(user.LogName + " disconnected a wire from " + + GameServer.Log(GameServer.CharacterLogName(user) + " disconnected a wire from " + connections[0].Item.Name + " (" + connections[0].Name + ") to "+ connections[1].Item.Name + " (" + connections[1].Name + ")", ServerLog.MessageType.ItemInteraction); } else if (connections[0] != null) { - GameServer.Log(user.LogName + " disconnected a wire from " + + GameServer.Log(GameServer.CharacterLogName(user) + " disconnected a wire from " + connections[0].Item.Name + " (" + connections[0].Name + ")", ServerLog.MessageType.ItemInteraction); } else if (connections[1] != null) { - GameServer.Log(user.LogName + " disconnected a wire from " + + GameServer.Log(GameServer.CharacterLogName(user) + " disconnected a wire from " + connections[1].Item.Name + " (" + connections[1].Name + ")", ServerLog.MessageType.ItemInteraction); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 2262d3de7..ee6c8a0d0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -4,10 +4,11 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Globalization; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; using Barotrauma.Extensions; +using FarseerPhysics.Dynamics; namespace Barotrauma.Items.Components { @@ -35,8 +36,16 @@ namespace Barotrauma.Items.Components private int failedLaunchAttempts; private readonly List activeProjectiles = new List(); + public IEnumerable ActiveProjectiles => activeProjectiles; private Character user; + + private float resetUserTimer; + + public float Rotation + { + get { return rotation; } + } [Serialize("0,0", false, description: "The position of the barrel relative to the upper left corner of the base sprite (in pixels).")] public Vector2 BarrelPos @@ -74,7 +83,28 @@ namespace Barotrauma.Items.Components set { reloadTime = value; } } - [Serialize(1, false, description: "How projectiles the weapon launches when fired once.")] + [Editable(0.1f, 10f), Serialize(1.0f, false, description: "Modifies the duration of retraction of the barrell after recoil to get back to the original position after shooting. Reload time affects this too.")] + public float RetractionDurationMultiplier + { + get; + set; + } + + [Editable(0.1f, 10f), Serialize(0.1f, false, description: "How quickly the recoil moves the barrel after launching.")] + public float RecoilTime + { + get; + set; + } + + [Editable(0f, 1000f), Serialize(0f, false, description: "How long the barrell stays in place after the recoil and before retracting back to the original position.")] + public float RetractionDelay + { + get; + set; + } + + [Serialize(1, false, description: "How many projectiles the weapon launches when fired once.")] public int ProjectileCount { get; @@ -88,7 +118,8 @@ namespace Barotrauma.Items.Components set; } - [Editable, Serialize("0.0,0.0", true, description: "The range at which the barrel can rotate. TODO")] + [Editable(VectorComponentLabels = new string[] { "editable.minvalue", "editable.maxvalue" }), + Serialize("0.0,0.0", true, description: "The range at which the barrel can rotate.", alwaysUseInstanceValues: true)] public Vector2 RotationLimits { get @@ -168,7 +199,7 @@ namespace Barotrauma.Items.Components } private float baseRotationRad; - [Editable(0.0f, 360.0f), Serialize(0.0f, true, description: "The angle of the turret's base in degrees.")] + [Editable(0.0f, 360.0f), Serialize(0.0f, true, description: "The angle of the turret's base in degrees.", alwaysUseInstanceValues: true)] public float BaseRotation { get { return MathHelper.ToDegrees(baseRotationRad); } @@ -224,6 +255,7 @@ namespace Barotrauma.Items.Components //if (item.FlippedY) flippedRotation = 180.0f - flippedRotation; transformedBarrelPos = MathUtils.RotatePointAroundTarget(barrelPos * item.Scale, new Vector2(item.Rect.Width / 2, item.Rect.Height / 2), flippedRotation); #if CLIENT + item.ResetCachedVisibleSize(); item.SpriteRotation = MathHelper.ToRadians(flippedRotation); #endif } @@ -252,6 +284,16 @@ namespace Barotrauma.Items.Components if (reload > 0.0f) { reload -= deltaTime; } + if (user != null && user.Removed) + { + user = null; + } + else + { + resetUserTimer -= deltaTime; + if (resetUserTimer <= 0.0f) { user = null; } + } + ApplyStatusEffects(ActionType.OnActive, deltaTime, null); UpdateProjSpecific(deltaTime); @@ -356,7 +398,7 @@ namespace Barotrauma.Items.Components { linkedItem.Use(deltaTime, null); var repairable = linkedItem.GetComponent(); - if (repairable != null) + if (repairable != null && failedLaunchAttempts < 2) { repairable.LastActiveTime = (float)Timing.TotalTime + 1.0f; } @@ -411,7 +453,7 @@ namespace Barotrauma.Items.Components #if SERVER if (character != null && launchedProjectile != null) { - string msg = character.LogName + " launched " + item.Name + " (projectile: " + launchedProjectile.Item.Name; + string msg = GameServer.CharacterLogName(character) + " launched " + item.Name + " (projectile: " + launchedProjectile.Item.Name; var containedItems = launchedProjectile.Item.ContainedItems; if (containedItems == null || !containedItems.Any()) { @@ -428,7 +470,7 @@ namespace Barotrauma.Items.Components return true; } - private void Launch(Item projectile, Character user = null) + private void Launch(Item projectile, Character user = null, float? launchRotation = null) { reload = reloadTime; @@ -444,7 +486,9 @@ namespace Barotrauma.Items.Components } float spread = MathHelper.ToRadians(Spread) * Rand.Range(-0.5f, 0.5f); - projectile.SetTransform(ConvertUnits.ToSimUnits(new Vector2(item.WorldRect.X + transformedBarrelPos.X, item.WorldRect.Y - transformedBarrelPos.Y)), -rotation + spread); + projectile.SetTransform( + ConvertUnits.ToSimUnits(new Vector2(item.WorldRect.X + transformedBarrelPos.X, item.WorldRect.Y - transformedBarrelPos.Y)), + -(launchRotation ?? rotation) + spread); projectile.UpdateTransform(); projectile.Submarine = projectile.body?.Submarine; @@ -456,12 +500,11 @@ namespace Barotrauma.Items.Components projectileComponent.User = user; } - if (projectile.Container != null) { projectile.Container.RemoveContained(projectile); } - - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) - { - GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ComponentState, item.GetComponentIndex(this), projectile }); - } + if (projectile.Container != null) { projectile.Container.RemoveContained(projectile); } + } + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + { + GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ComponentState, item.GetComponentIndex(this), projectile }); } ApplyStatusEffects(ActionType.OnUse, 1.0f, user: user); @@ -476,8 +519,10 @@ namespace Barotrauma.Items.Components private float prevTargetRotation; private float updateTimer; private bool updatePending; - public void ThalamusOperate(float deltaTime, bool targetHumans, bool targetOtherCreatures, bool targetSubmarines, bool ignoreDelay) + public void ThalamusOperate(WreckAI ai, float deltaTime, bool targetHumans, bool targetOtherCreatures, bool targetSubmarines, bool ignoreDelay) { + if (ai == null) { return; } + IsActive = true; if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) @@ -513,8 +558,8 @@ namespace Barotrauma.Items.Components foreach (var character in Character.CharacterList) { if (character == null || character.Removed || character.IsDead) { continue; } - if (character.Params.Group.Equals("thalamus", StringComparison.OrdinalIgnoreCase)) { continue; } - bool isHuman = character.IsHuman || character.Params.Group.Equals("human", StringComparison.OrdinalIgnoreCase); + if (character.Params.Group.Equals(ai.Config.Entity, StringComparison.OrdinalIgnoreCase)) { continue; } + bool isHuman = character.IsHuman || character.Params.Group.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase); if (isHuman) { if (!targetHumans) @@ -619,7 +664,8 @@ namespace Barotrauma.Items.Components end -= target.Submarine.SimPosition; } var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel; - var pickedBody = Submarine.PickBody(start, end, null, collisionCategories); + var pickedBody = Submarine.PickBody(start, end, null, collisionCategories, allowInsideFixture: true, + customPredicate: (Fixture f) => { return !item.StaticFixtures.Contains(f); }); if (pickedBody == null) { return; } Character targetCharacter = null; if (pickedBody.UserData is Character c) @@ -632,16 +678,28 @@ namespace Barotrauma.Items.Components } if (targetCharacter != null) { - if (targetCharacter.Params.Group.Equals("thalamus", StringComparison.OrdinalIgnoreCase)) + if (targetCharacter.Params.Group.Equals(ai.Config.Entity, StringComparison.OrdinalIgnoreCase)) { // Don't shoot friendly characters return; } } - else if (!(pickedBody.UserData is Structure) && !(pickedBody.UserData is Item)) + else { - // Hit something else than a wall or an item (probably a level wall) - return; + if (pickedBody.UserData is ISpatialEntity e) + { + Submarine sub = e.Submarine; + if (sub == null) { return; } + if (!targetSubmarines) { return; } + if (sub == Item.Submarine) { return; } + // Don't shoot non-player submarines, i.e. wrecks or outposts. + if (!sub.Info.IsPlayer) { return; } + } + else + { + // Hit something else, probably a level wall + return; + } } TryLaunch(deltaTime, ignorePower: true); } @@ -663,6 +721,7 @@ namespace Barotrauma.Items.Components PowerContainer batteryToLoad = null; foreach (PowerContainer battery in batteries) { + if (battery.Item.NonInteractable) { continue; } if (batteryToLoad == null || battery.Charge < lowestCharge) { batteryToLoad = battery; @@ -683,20 +742,22 @@ namespace Barotrauma.Items.Components int maxProjectileCount = 0; foreach (MapEntity e in item.linkedTo) { - if (!(e is Item projectileContainer)) continue; - - var containedItems = projectileContainer.ContainedItems; - if (containedItems != null) + if (item.NonInteractable) { continue; } + if (e is Item projectileContainer) { - var container = projectileContainer.GetComponent(); - maxProjectileCount += container.Capacity; + var containedItems = projectileContainer.ContainedItems; + if (containedItems != null) + { + var container = projectileContainer.GetComponent(); + maxProjectileCount += container.Capacity; - int projectiles = containedItems.Count(it => it.Condition > 0.0f); - usableProjectileCount += projectiles; + int projectiles = containedItems.Count(it => it.Condition > 0.0f); + usableProjectileCount += projectiles; + } } } - if (usableProjectileCount == 0 || (usableProjectileCount < maxProjectileCount && objective.Option.Equals("fireatwill", StringComparison.OrdinalIgnoreCase))) + if (usableProjectileCount == 0) { ItemContainer container = null; Item containerItem = null; @@ -704,11 +765,16 @@ namespace Barotrauma.Items.Components { containerItem = e as Item; if (containerItem == null) { continue; } + if (containerItem.NonInteractable) { continue; } + if (character.AIController is HumanAIController aiController && aiController.IgnoredItems.Contains(containerItem)) { continue; } container = containerItem.GetComponent(); if (container != null) { break; } } - if (container == null || container.ContainableItems.Count == 0) { return true; } - + if (container == null || container.ContainableItems.Count == 0) + { + character.Speak(TextManager.GetWithVariable("DialogCannotLoadTurret", "[itemname]", item.Name, true), null, 0.0f, "cannotloadturret", 30.0f); + return true; + } if (objective.SubObjectives.None()) { if (!AIDecontainEmptyItems(character, objective, equip: true, sourceContainer: container)) @@ -719,10 +785,25 @@ namespace Barotrauma.Items.Components if (objective.SubObjectives.None()) { var loadItemsObjective = AIContainItems(container, character, objective, usableProjectileCount + 1, equip: true, removeEmpty: true); - loadItemsObjective.ignoredContainerIdentifiers = new string[] { containerItem.prefab.Identifier }; - character.Speak(TextManager.GetWithVariable("DialogLoadTurret", "[itemname]", item.Name, true), null, 0.0f, "loadturret", 30.0f); + if (loadItemsObjective == null) + { + if (usableProjectileCount == 0) + { + character.Speak(TextManager.GetWithVariable("DialogCannotLoadTurret", "[itemname]", item.Name, true), null, 0.0f, "cannotloadturret", 30.0f); + return true; + } + } + else + { + loadItemsObjective.ignoredContainerIdentifiers = new string[] { containerItem.prefab.Identifier }; + character.Speak(TextManager.GetWithVariable("DialogLoadTurret", "[itemname]", item.Name, true), null, 0.0f, "loadturret", 30.0f); + return false; + } + } + if (objective.SubObjectives.Any()) + { + return false; } - return false; } //enough shells and power @@ -753,7 +834,10 @@ namespace Barotrauma.Items.Components character.AIController.SelectTarget(closestEnemy.AiTarget); character.CursorPosition = closestEnemy.WorldPosition; - if (item.Submarine != null) { character.CursorPosition -= item.Submarine.Position; } + if (character.Submarine != null) + { + character.CursorPosition -= character.Submarine.Position; + } float enemyAngle = MathUtils.VectorToAngle(closestEnemy.WorldPosition - item.WorldPosition); float turretAngle = -rotation; @@ -769,7 +853,8 @@ namespace Barotrauma.Items.Components end -= closestEnemy.Submarine.SimPosition; } var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel; - var pickedBody = Submarine.PickBody(start, end, null, collisionCategories); + var pickedBody = Submarine.PickBody(start, end, null, collisionCategories, allowInsideFixture: true, + customPredicate: (Fixture f) => { return !item.StaticFixtures.Contains(f); }); if (pickedBody == null) { return false; } Character targetCharacter = null; if (pickedBody.UserData is Character c) @@ -780,23 +865,34 @@ namespace Barotrauma.Items.Components { targetCharacter = limb.character; } - if (targetCharacter != null && HumanAIController.IsFriendly(character, targetCharacter)) + if (targetCharacter != null) { - // Don't shoot friendly characters - return false; + if (HumanAIController.IsFriendly(character, targetCharacter)) + { + // Don't shoot friendly characters + return false; + } } - else if (targetCharacter == null && !(pickedBody.UserData is Structure) && !(pickedBody.UserData is Item)) + else { - // Hit something else than a wall or an item (probably a level wall) - return false; + if (pickedBody.UserData is ISpatialEntity e) + { + Submarine sub = e.Submarine; + if (sub == null) { return false; } + if (sub == Item.Submarine) { return false; } + // Don't shoot non-player submarines, i.e. wrecks or outposts. + if (!sub.Info.IsPlayer) { return false; } + // Don't shoot friendly submarines. + if (sub.TeamID == Item.Submarine.TeamID) { return false; } + } + else + { + // Hit something else, probably a level wall + return false; + } } - - if (objective.Option.Equals("fireatwill", StringComparison.OrdinalIgnoreCase)) - { - character?.Speak(TextManager.GetWithVariable("DialogFireTurret", "[itemname]", item.Name, true), null, 0.0f, "fireturret", 5.0f); - character.SetInput(InputType.Shoot, true, true); - } - + character?.Speak(TextManager.GetWithVariable("DialogFireTurret", "[itemname]", item.Name, true), null, 0.0f, "fireturret", 5.0f); + character.SetInput(InputType.Shoot, true, true); return false; } @@ -914,10 +1010,12 @@ namespace Barotrauma.Items.Components IsActive = true; } user = sender; + resetUserTimer = 10.0f; break; case "trigger_in": item.Use((float)Timing.Step, sender); user = sender; + resetUserTimer = 10.0f; //triggering the Use method through item.Use will fail if the item is not characterusable and the signal was sent by a character //so lets do it manually if (!characterUsable && sender != null) @@ -936,9 +1034,25 @@ namespace Barotrauma.Items.Components public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { - Item item = extraData.Length > 2 ? (Item)extraData[2] : null; - msg.Write(item == null || item.Removed ? (ushort)0 : item.ID); - msg.WriteRangedSingle(MathHelper.Clamp(targetRotation, minRotation, maxRotation), minRotation, maxRotation, 8); + if (extraData.Length > 2) + { + msg.Write(!(extraData[2] is Item item) || item.Removed ? ushort.MaxValue : item.ID); + msg.WriteRangedSingle(MathHelper.Clamp(rotation, minRotation, maxRotation), minRotation, maxRotation, 16); + } + else + { + msg.Write((ushort)0); + float wrappedTargetRotation = targetRotation; + while (wrappedTargetRotation < minRotation && MathUtils.IsValid(wrappedTargetRotation)) + { + wrappedTargetRotation += MathHelper.TwoPi; + } + while (wrappedTargetRotation > maxRotation && MathUtils.IsValid(wrappedTargetRotation)) + { + wrappedTargetRotation -= MathHelper.TwoPi; + } + msg.WriteRangedSingle(MathHelper.Clamp(wrappedTargetRotation, minRotation, maxRotation), minRotation, maxRotation, 16); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index c95adedb1..fd717b1c2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -1,7 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; using Barotrauma.Items.Components; @@ -221,11 +221,11 @@ namespace Barotrauma.Items.Components get { return variant; } set { -#if SERVER + if (variant == value) { return; } +#if SERVER variant = value; item.CreateServerEvent(this); #elif CLIENT - if (variant == value) { return; } Character character = picker; if (character != null) @@ -370,11 +370,12 @@ namespace Barotrauma.Items.Components public override void Unequip(Character character) { - if (picker == null) return; + if (character == null || character.Removed) { return; } + if (picker == null) { return; } for (int i = 0; i < wearableSprites.Length; i++) { Limb equipLimb = character.AnimController.GetLimb(limbType[i]); - if (equipLimb == null) continue; + if (equipLimb == null) { continue; } if (wearableSprites[i].LightComponent != null) { @@ -385,7 +386,6 @@ namespace Barotrauma.Items.Components #if CLIENT equipLimb.UpdateWearableTypesToHide(); #endif - limb[i] = null; } @@ -419,9 +419,14 @@ namespace Barotrauma.Items.Components { base.RemoveComponentSpecific(); + Unequip(picker); + foreach (WearableSprite wearableSprite in wearableSprites) { - if (wearableSprite != null && wearableSprite.Sprite != null) wearableSprite.Sprite.Remove(); + if (wearableSprite != null && wearableSprite.Sprite != null) + { + wearableSprite.Sprite.Remove(); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index 98eb99010..c54233341 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -54,6 +54,33 @@ namespace Barotrauma #endif } + public static Item FindItemRecursive(Item item, Predicate condition) + { + if (condition.Invoke(item)) + { + return item; + } + + var containers = item.GetComponents(); + + if (containers != null) + { + foreach (var container in containers) + { + foreach (var inventoryItem in container.Inventory.Items) + { + var findItem = FindItemRecursive(inventoryItem, condition); + if (findItem != null) + { + return findItem; + } + } + } + } + + return null; + } + public int FindIndex(Item item) { for (int i = 0; i < capacity; i++) @@ -173,6 +200,7 @@ namespace Barotrauma if (Owner == null) return; Inventory prevInventory = item.ParentInventory; + Inventory prevOwnerInventory = item.FindParentInventory(inv => inv is CharacterInventory); if (createNetworkEvent) { @@ -199,6 +227,14 @@ namespace Barotrauma item.body.Enabled = false; item.body.BodyType = FarseerPhysics.BodyType.Dynamic; } + +#if SERVER + if (prevOwnerInventory is CharacterInventory characterInventory && characterInventory != this && Owner == user) + { + var client = GameMain.Server?.ConnectedClients?.Find(cl => cl.Character == user); + GameMain.Server?.KarmaManager.OnItemTakenFromPlayer(characterInventory, client, item); + } +#endif } public bool IsEmpty() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 81e482f8f..2e24432e5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -17,17 +17,6 @@ using Microsoft.Xna.Framework.Graphics; namespace Barotrauma { - public enum ActionType - { - Always, OnPicked, OnUse, OnSecondaryUse, - OnWearing, OnContaining, OnContained, OnNotContained, - OnActive, OnFailure, OnBroken, - OnFire, InWater, NotInWater, - OnImpact, - OnEating, - OnDeath = OnBroken, - OnDamaged - } partial class Item : MapEntity, IDamageable, ISerializableEntity, IServerSerializable, IClientSerializable { @@ -38,6 +27,8 @@ namespace Barotrauma private HashSet tags; + private bool isWire; + private Hull currentHull; public Hull CurrentHull { @@ -63,11 +54,14 @@ namespace Barotrauma /// private readonly List updateableComponents = new List(); private List drawableComponents; + private bool hasComponentsToDraw; public PhysicsBody body; public readonly XElement StaticBodyConfig; + public List StaticFixtures = new List(); + private bool transformDirty = true; private float lastSentCondition; @@ -166,7 +160,7 @@ namespace Barotrauma set { description = value; } } - [Editable, Serialize(false, true)] + [Editable, Serialize(false, true, alwaysUseInstanceValues: true)] public bool NonInteractable { get; @@ -598,7 +592,7 @@ namespace Barotrauma spriteColor = prefab.SpriteColor; components = new List(); - drawableComponents = new List(); + drawableComponents = new List(); hasComponentsToDraw = false; tags = new HashSet(); repairables = new List(); @@ -626,7 +620,7 @@ namespace Barotrauma case "body": body = new PhysicsBody(subElement, ConvertUnits.ToSimUnits(Position), Scale); string collisionCategory = subElement.GetAttributeString("collisioncategory", null); - if (Prefab.DamagedByProjectiles || Prefab.DamagedByMeleeWeapons) + if ((Prefab.DamagedByProjectiles || Prefab.DamagedByMeleeWeapons) && Condition > 0) { //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 @@ -683,7 +677,11 @@ namespace Barotrauma AddComponent(ic); - if (ic is IDrawableComponent && ic.Drawable) drawableComponents.Add(ic as IDrawableComponent); + if (ic is IDrawableComponent && ic.Drawable) + { + drawableComponents.Add(ic as IDrawableComponent); + hasComponentsToDraw = true; + } if (ic is Repairable) repairables.Add((Repairable)ic); break; } @@ -762,6 +760,8 @@ namespace Barotrauma ItemList.Add(this); DebugConsole.Log("Created " + Name + " (" + ID + ")"); + + if (Components.All(ic => ic is Wire || ic is Holdable)) { isWire = true; } } partial void InitProjSpecific(); @@ -888,12 +888,23 @@ namespace Barotrauma if (!drawableComponents.Contains(drawable)) { drawableComponents.Add(drawable); + hasComponentsToDraw = true; +#if CLIENT + cachedVisibleSize = null; +#endif } } public void DisableDrawableComponent(IDrawableComponent drawable) { - drawableComponents.Remove(drawable); + if (drawableComponents.Contains(drawable)) + { + drawableComponents.Remove(drawable); + hasComponentsToDraw = drawableComponents.Count > 0; +#if CLIENT + cachedVisibleSize = null; +#endif + } } public int GetComponentIndex(ItemComponent component) @@ -1065,7 +1076,7 @@ namespace Barotrauma public Item GetRootContainer() { - if (Container == null) return null; + if (Container == null) { return null; } Item rootContainer = Container; while (rootContainer.Container != null) @@ -1076,7 +1087,16 @@ namespace Barotrauma return rootContainer; } - public bool IsOwnedBy(Character character) => FindParentInventory(i => i.Owner == character) != null; + public bool IsOwnedBy(Entity entity) => FindParentInventory(i => i.Owner == entity) != null; + + public Entity GetRootInventoryOwner() + { + if (ParentInventory == null) { return this; } + if (ParentInventory.Owner is Character) { return ParentInventory.Owner; } + var rootContainer = GetRootContainer(); + if (rootContainer?.ParentInventory?.Owner is Character) { return rootContainer.ParentInventory.Owner; } + return rootContainer ?? this; + } public Inventory FindParentInventory(Func predicate) { @@ -1104,16 +1124,28 @@ namespace Barotrauma public void AddTag(string tag) { - if (tags.Contains(tag)) return; + if (tags.Contains(tag)) { return; } tags.Add(tag); } public bool HasTag(string tag) { - if (tag == null) return true; + if (tag == null) { return true; } return tags.Contains(tag) || prefab.Tags.Contains(tag); } + public void ReplaceTag(string tag, string newTag) + { + if (!tags.Contains(tag)) { return; } + tags.Remove(tag); + tags.Add(newTag); + } + + public IEnumerable GetTags() + { + return tags; + } + public bool HasTag(IEnumerable allowedTags) { if (allowedTags == null) return true; @@ -1391,7 +1423,7 @@ namespace Barotrauma } else { - if (updateableComponents.Count == 0 && aiTarget == null && !hasStatusEffectsOfType[(int)ActionType.Always] && body == null) + if (updateableComponents.Count == 0 && aiTarget == null && !hasStatusEffectsOfType[(int)ActionType.Always] && (body == null || !body.Enabled)) { #if CLIENT positionBuffer.Clear(); @@ -1423,6 +1455,14 @@ namespace Barotrauma body.SetTransform(body.SimPosition + prevSub.SimPosition - Submarine.SimPosition, body.Rotation); } + if (Submarine != prevSub && ContainedItems != null) + { + foreach (Item containedItem in ContainedItems) + { + containedItem.Submarine = Submarine; + } + } + Vector2 displayPos = ConvertUnits.ToDisplayUnits(body.SimPosition); rect.X = (int)(displayPos.X - rect.Width / 2.0f); rect.Y = (int)(displayPos.Y + rect.Height / 2.0f); @@ -1671,7 +1711,22 @@ namespace Barotrauma } } } - + + public Controller FindController() + { + //try finding the controller with the simpler non-recursive method first + var controllers = GetConnectedComponents(); + if (controllers.None()) { controllers = GetConnectedComponents(recursive: true); } + return controllers.Count < 2 ? controllers.FirstOrDefault() : + (controllers.FirstOrDefault(c => c.GetFocusTarget() == this) ?? controllers.FirstOrDefault()); + } + + public bool TryFindController(out Controller controller) + { + controller = FindController(); + return controller != null; + } + public void SendSignal(int stepsTaken, string signal, string connectionName, Character sender, float power = 0.0f, Item source = null, float signalStrength = 1.0f) { if (connections == null) { return; } @@ -1745,7 +1800,8 @@ namespace Barotrauma #if CLIENT bool hasRequiredSkills = true; Skill requiredSkill = null; -#endif +#endif + if (NonInteractable) { return false; } foreach (ItemComponent ic in components) { bool pickHit = false, selectHit = false; @@ -2003,6 +2059,8 @@ namespace Barotrauma public void Drop(Character dropper, bool createNetworkEvent = true) { + Inventory prevInventory = parentInventory; + if (createNetworkEvent) { if (parentInventory != null && !parentInventory.Owner.Removed && !Removed && @@ -2016,6 +2074,7 @@ namespace Barotrauma if (body != null) { + isActive = true; body.Enabled = true; body.PhysEnabled = true; body.ResetDynamics(); @@ -2477,7 +2536,7 @@ namespace Barotrauma return; } DebugConsole.Log("Removing item " + Name + " (ID: " + ID + ")"); - + base.Remove(); foreach (Character character in Character.CharacterList) @@ -2507,6 +2566,18 @@ namespace Barotrauma body = null; } + if (StaticFixtures != null) + { + foreach (Fixture fixture in StaticFixtures) + { + //if the world is null, the body has already been removed + //happens if the sub the fixture is attached to is removed before the item + if (fixture.Body?.World == null) { continue; } + fixture.Body.Remove(fixture); + } + StaticFixtures.Clear(); + } + foreach (Item it in ItemList) { if (it.linkedTo.Contains(this)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index b29a31ad5..737acecc0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -2,7 +2,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Xml.Linq; using System.Linq; using Barotrauma.Items.Components; @@ -223,6 +223,12 @@ namespace Barotrauma private set; } + public bool AllowDeconstruct + { + get; + private set; + } + //how close the Character has to be to the item to pick it up [Serialize(120.0f, false)] public float InteractDistance @@ -246,6 +252,9 @@ namespace Barotrauma private set; } + [Serialize(false, false, description: "Hides the condition bar displayed at the bottom of the inventory slot the item is in.")] + public bool HideConditionBar { get; set; } + //if true and the item has trigger areas defined, characters need to be within the trigger to interact with the item //if false, trigger areas define areas that can be used to highlight the item [Serialize(true, false)] @@ -300,6 +309,13 @@ namespace Barotrauma private set; } + [Serialize(1f, false)] + public float ExplosionDamageMultiplier + { + get; + private set; + } + [Serialize(false, false)] public bool DamagedByProjectiles { @@ -314,6 +330,20 @@ namespace Barotrauma private set; } + [Serialize(false, false)] + public bool DamagedByRepairTools + { + get; + private set; + } + + [Serialize(false, false)] + public bool DamagedByMonsters + { + get; + private set; + } + [Serialize(false, false)] public bool FireProof { @@ -727,7 +757,7 @@ namespace Barotrauma #endif case "deconstruct": DeconstructTime = subElement.GetAttributeFloat("time", 1.0f); - + AllowDeconstruct = true; foreach (XElement deconstructItem in subElement.Elements()) { if (deconstructItem.Attribute("name") != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/CorpsePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/CorpsePrefab.cs index 07011e517..1e0d220d2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/CorpsePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/CorpsePrefab.cs @@ -155,16 +155,16 @@ namespace Barotrauma Prefabs.RemoveByFile(filePath); } - public void GiveItems(Character character) + public void GiveItems(Character character, Submarine submarine) { var spawnItems = ToolBox.SelectWeightedRandom(ItemSets.Keys.ToList(), ItemSets.Values.ToList(), Rand.RandSync.Unsynced); foreach (XElement itemElement in spawnItems.GetChildElements("item")) { - InitializeItems(character, itemElement); + InitializeItems(character, itemElement, submarine); } } - private void InitializeItems(Character character, XElement itemElement, Item parentItem = null) + private void InitializeItems(Character character, XElement itemElement, Submarine submarine, Item parentItem = null) { ItemPrefab itemPrefab; string itemIdentifier = itemElement.GetAttributeString("identifier", ""); @@ -201,9 +201,10 @@ namespace Barotrauma { character.Inventory.TryPutItem(item, null, item.AllowedSlots); } - if (item.Prefab.Identifier == "idcard") + if (item.Prefab.Identifier == "idcard" || item.Prefab.Identifier == "idcardwreck") { item.AddTag("name:" + character.Name); + item.ReplaceTag("wreck_id", Level.Loaded.GetWreckIDTag("wreck_id", submarine)); var job = character.Info?.Job; if (job != null) { @@ -220,7 +221,7 @@ namespace Barotrauma } foreach (XElement childItemElement in itemElement.Elements()) { - InitializeItems(character, childItemElement, item); + InitializeItems(character, childItemElement, submarine, item); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs index 1ab304ba9..affe3215e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Entity.cs @@ -1,7 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Text; @@ -146,7 +146,7 @@ namespace Barotrauma { id += 1; IDfound = dictionary.ContainsKey(id); - } while (IDfound); + } while (IDfound || id == NullEntityID || id == EntitySpawnerID); return id; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index b69253f6f..bd475290f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -30,9 +30,9 @@ namespace Barotrauma public float EmpStrength { get; set; } - public Explosion(float range, float force, float damage, float structureDamage, float empStrength = 0.0f) + public Explosion(float range, float force, float damage, float structureDamage, float itemDamage, float empStrength = 0.0f) { - attack = new Attack(damage, 0.0f, 0.0f, structureDamage, range) + attack = new Attack(damage, 0.0f, 0.0f, structureDamage, itemDamage, range) { SeverLimbsProbability = 1.0f }; @@ -157,42 +157,47 @@ namespace Barotrauma if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) { - if (flames) + foreach (Item item in Item.ItemList) { - foreach (Item item in Item.ItemList) + if (item.Condition <= 0.0f) { continue; } + if (Vector2.Distance(item.WorldPosition, worldPosition) > attack.Range * 0.5f) { continue; } + if (flames && !item.FireProof) { - 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) { fireProof = true; break; } + if (container.FireProof) + { + fireProof = true; + break; + } container = container.Container; } - - 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) + if (!fireProof) { - GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFire }); + 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) + 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) { - 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; + continue; } + float distFactor = 1.0f - dist / attack.Range; + float damageAmount = attack.GetItemDamage(1.0f) * item.Prefab.ExplosionDamageMultiplier; + item.Condition -= damageAmount * distFactor; } } } @@ -200,10 +205,9 @@ namespace Barotrauma partial void ExplodeProjSpecific(Vector2 worldPosition, Hull hull); - public static void DamageCharacters(Vector2 worldPosition, Attack attack, float force, Entity damageSource, Character attacker) { - if (attack.Range <= 0.0f) return; + if (attack.Range <= 0.0f) { return; } //long range for the broad distance check, because large characters may still be in range even if their collider isn't float broadRange = Math.Max(attack.Range * 10.0f, 10000.0f); @@ -220,19 +224,20 @@ namespace Barotrauma Vector2 explosionPos = worldPosition; if (c.Submarine != null) { explosionPos -= c.Submarine.Position; } - Hull hull = Hull.FindHull(ConvertUnits.ToDisplayUnits(explosionPos), null, false); + Hull hull = Hull.FindHull(explosionPos, null, false); bool underWater = hull == null || explosionPos.Y < hull.Surface; explosionPos = ConvertUnits.ToSimUnits(explosionPos); Dictionary distFactors = new Dictionary(); + Dictionary damages = new Dictionary(); foreach (Limb limb in c.AnimController.Limbs) { float dist = Vector2.Distance(limb.WorldPosition, worldPosition); //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); + float limbRadius = limb.body.GetMaxExtent(); dist = Math.Max(0.0f, dist - ConvertUnits.ToDisplayUnits(limbRadius)); if (dist > attack.Range) { continue; } @@ -240,14 +245,18 @@ namespace Barotrauma float distFactor = 1.0f - dist / attack.Range; //solid obstacles between the explosion and the limb reduce the effect of the explosion by 90% - if (Submarine.CheckVisibility(limb.SimPosition, explosionPos) != null) distFactor *= 0.1f; + if (Submarine.CheckVisibility(limb.SimPosition, explosionPos) != null) + { + distFactor *= 0.1f; + } distFactors.Add(limb, distFactor); List modifiedAfflictions = new List(); + int limbCount = c.AnimController.Limbs.Count(l => !l.IsSevered && !l.ignoreCollisions); foreach (Affliction affliction in attack.Afflictions.Keys) { - modifiedAfflictions.Add(affliction.CreateMultiplied(distFactor / c.AnimController.Limbs.Length)); + modifiedAfflictions.Add(affliction.CreateMultiplied(distFactor / limbCount)); } c.LastDamageSource = damageSource; if (attacker == null) @@ -255,14 +264,18 @@ namespace Barotrauma if (damageSource is Item item) { attacker = item.GetComponent()?.User; - if (attacker == null) attacker = item.GetComponent()?.User; + if (attacker == null) + { + attacker = item.GetComponent()?.User; + } } } //use a position slightly from the limb's position towards the explosion //ensures that the attack hits the correct limb and that the direction of the hit can be determined correctly in the AddDamage methods Vector2 hitPos = limb.WorldPosition + (worldPosition - limb.WorldPosition) / dist * 0.01f; - c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, false, attacker: attacker); + AttackResult attackResult = c.AddDamage(hitPos, modifiedAfflictions, attack.Stun * distFactor, false, attacker: attacker); + damages.Add(limb, attackResult.Damage); if (attack.StatusEffects != null && attack.StatusEffects.Any()) { @@ -287,14 +300,19 @@ namespace Barotrauma } //sever joints - if (c.IsDead && attack.SeverLimbsProbability > 0.0f) + if (attack.SeverLimbsProbability > 0.0f) { foreach (Limb limb in c.AnimController.Limbs) { - if (!distFactors.ContainsKey(limb)) { continue; } - if (Rand.Range(0.0f, 1.0f) < attack.SeverLimbsProbability * distFactors[limb]) + if (limb.character.Removed || limb.Removed) { continue; } + if (limb.IsSevered) { continue; } + if (!c.IsDead && !limb.CanBeSeveredAlive) { continue; } + if (distFactors.TryGetValue(limb, out float distFactor)) { - c.TrySeverLimbJoints(limb, 1.0f); + if (damages.TryGetValue(limb, out float damage)) + { + c.TrySeverLimbJoints(limb, attack.SeverLimbsProbability * distFactor, damage); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs index 08feb0f1c..0e0b69192 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs @@ -2,12 +2,14 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; +using Barotrauma.Extensions; #if CLIENT using Barotrauma.Sounds; using Barotrauma.Lights; using Barotrauma.Particles; #endif using FarseerPhysics; +using System.Linq; namespace Barotrauma { @@ -15,6 +17,7 @@ namespace Barotrauma { const float OxygenConsumption = 50.0f; const float GrowSpeed = 20.0f; + const float MaxDamageRange = 250.0f; protected Hull hull; @@ -65,7 +68,7 @@ namespace Barotrauma public virtual float DamageRange { - get { return (float)Math.Sqrt(size.X) * 20.0f; } + get { return Math.Min((float)Math.Sqrt(size.X) * 10.0f, MaxDamageRange); } } public Hull Hull @@ -123,8 +126,8 @@ namespace Barotrauma { i = Math.Min(i, fireSources.Count - 1); j = Math.Min(j, i - 1); - - if (!fireSources[i].CheckOverLap(fireSources[j])) continue; + + if (!fireSources[i].CheckOverLap(fireSources[j])) { continue; } float leftEdge = Math.Min(fireSources[i].position.X, fireSources[j].position.X); @@ -133,12 +136,10 @@ namespace Barotrauma - leftEdge; fireSources[j].position.X = leftEdge; - #if CLIENT fireSources[j].burnDecals.AddRange(fireSources[i].burnDecals); fireSources[j].burnDecals.Sort((d1, d2) => { return Math.Sign(d1.WorldPosition.X - d2.WorldPosition.X); }); #endif - fireSources[i].Remove(); } } @@ -210,21 +211,32 @@ namespace Barotrauma private void DamageCharacters(float deltaTime) { - if (size.X <= 0.0f) return; + if (size.X <= 0.0f) { return; } for (int i = 0; i < Character.CharacterList.Count; i++) { Character c = Character.CharacterList[i]; - if (c.AnimController.CurrentHull == null || c.IsDead) continue; + if (c.CurrentHull == null || c.IsDead) { continue; } - if (!IsInDamageRange(c, DamageRange)) continue; + if (!IsInDamageRange(c, DamageRange)) { continue; } - float dmg = (float)Math.Sqrt(size.X) * deltaTime / c.AnimController.Limbs.Length; + //GetApproximateDistance returns float.MaxValue if there's no path through open gaps between the hulls (e.g. if there's a door/wall in between) + if (hull.GetApproximateDistance(Position, c.Position, c.CurrentHull, 10000.0f) > size.X + DamageRange) + { + return; + } + + float dmg = (float)Math.Sqrt(Math.Min(500, size.X)) * deltaTime / c.AnimController.Limbs.Count(l => !l.IsSevered); foreach (Limb limb in c.AnimController.Limbs) { + if (limb.IsSevered) { continue; } c.LastDamageSource = null; - c.DamageLimb(WorldPosition, limb, new List() { AfflictionPrefab.Burn.Instantiate(dmg) }, 0.0f, false, 0.0f); + c.DamageLimb(WorldPosition, limb, AfflictionPrefab.Burn.Instantiate(dmg).ToEnumerable(), 0.0f, false, 0.0f); } +#if CLIENT + //let clients display the client-side damage immediately, otherwise they may not be able to react to the damage fast enough + c.CharacterHealth.DisplayedVitality = c.Vitality; +#endif c.ApplyStatusEffects(ActionType.OnFire, deltaTime); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index 73f7a993a..c0581a19a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -33,6 +33,8 @@ namespace Barotrauma //the force of the water flow which is exerted on physics bodies private Vector2 flowForce; private Hull flowTargetHull; + + private float openedTimer = 1.0f; private float higherSurface; private float lowerSurface; @@ -54,7 +56,11 @@ namespace Barotrauma public float Open { get { return open; } - set { open = MathHelper.Clamp(value, 0.0f, 1.0f); } + set + { + if (value > open) { openedTimer = 1.0f; } + open = MathHelper.Clamp(value, 0.0f, 1.0f); + } } public float Size => IsHorizontal ? Rect.Height : Rect.Width; @@ -124,6 +130,7 @@ namespace Barotrauma InsertToList(); outsideCollisionBlocker = GameMain.World.CreateEdge(-Vector2.UnitX * 2.0f, Vector2.UnitX * 2.0f); + outsideCollisionBlocker.UserData = $"CollisionBlocker (Gap {ID})"; outsideCollisionBlocker.BodyType = BodyType.Static; outsideCollisionBlocker.CollisionCategories = Physics.CollisionWall; outsideCollisionBlocker.CollidesWith = Physics.CollisionCharacter; @@ -277,57 +284,18 @@ namespace Barotrauma flowForce.X = MathHelper.Clamp(flowForce.X, -MaxFlowForce, MaxFlowForce); flowForce.Y = MathHelper.Clamp(flowForce.Y, -MaxFlowForce, MaxFlowForce); - lerpedFlowForce = Vector2.Lerp(lerpedFlowForce, flowForce, deltaTime * 5.0f); + if (openedTimer > 0.0f && flowForce.Length() > lerpedFlowForce.Length()) + { + //if the gap has just been opened/created, allow it to exert a large force instantly without any smoothing + lerpedFlowForce = flowForce; + } + else + { + lerpedFlowForce = Vector2.Lerp(lerpedFlowForce, flowForce, deltaTime * 5.0f); + } + openedTimer -= deltaTime; EmitParticles(deltaTime); - - if (flowTargetHull != null && lerpedFlowForce.LengthSquared() > 0.0001f) - { - foreach (Character character in Character.CharacterList) - { - if (character.CurrentHull == null) continue; - if (character.CurrentHull != linkedTo[0] as Hull && - (linkedTo.Count < 2 || character.CurrentHull != linkedTo[1] as Hull)) - { - continue; - } - - foreach (Limb limb in character.AnimController.Limbs) - { - if (!limb.inWater) continue; - - float dist = Vector2.Distance(limb.WorldPosition, WorldPosition); - if (dist > lerpedFlowForce.Length()) continue; - - Vector2 force = lerpedFlowForce / (float)Math.Max(Math.Sqrt(dist), 20.0f) * 0.025f; - - //vertical gaps only apply forces if the character is roughly above/below the gap - if (!IsHorizontal) - { - float xDist = Math.Abs(limb.WorldPosition.X - WorldPosition.X); - if (xDist > rect.Width || rect.Width == 0) break; - - force *= 1.0f - xDist / rect.Width; - } - - if (!MathUtils.IsValid(force)) - { - string errorMsg = "Attempted to apply invalid flow force to the character \"" + character.Name + - "\", gap pos: " + WorldPosition + - ", limb pos: " + limb.WorldPosition + - ", flowforce: " + flowForce + ", lerpedFlowForce:" + lerpedFlowForce + - ", dist: " + dist; - - DebugConsole.Log(errorMsg); - GameAnalyticsManager.AddErrorEventOnce("Gap.Update:InvalidFlowForce:" + character.Name, - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - errorMsg); - continue; - } - character.AnimController.Collider.ApplyForce(force * limb.body.Mass, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); - } - } - } } partial void EmitParticles(float deltaTime); @@ -510,7 +478,7 @@ namespace Barotrauma //the larger the gap is, the faster the water flows float sizeModifier = size * open * open; - float delta = hull1.Volume * Hull.MaxCompress * sizeModifier * deltaTime; + float delta = 500.0f * sizeModifier * deltaTime; //make sure not to place more water to the target room than it can hold delta = Math.Min(delta, hull1.Volume * Hull.MaxCompress - hull1.WaterVolume); @@ -595,7 +563,7 @@ namespace Barotrauma private void UpdateOutsideColliderPos(Hull hull) { - if (Submarine == null || IsRoomToRoom) { return; } + if (Submarine == null || IsRoomToRoom || Level.Loaded == null) { return; } Vector2 rayDir; if (IsHorizontal) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 3de38ad57..d840a1b80 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -22,9 +22,9 @@ namespace Barotrauma public const float OxygenConsumptionSpeed = 700.0f; public const int WaveWidth = 32; - public static float WaveStiffness = 0.02f; - public static float WaveSpread = 0.05f; - public static float WaveDampening = 0.05f; + public static float WaveStiffness = 0.01f; + public static float WaveSpread = 0.02f; + public static float WaveDampening = 0.02f; //how much excess water the room can contain, relative to the volume of the room. //needed to make it possible for pressure to "push" water up through U-shaped hull configurations @@ -83,6 +83,21 @@ namespace Barotrauma } } + private Color ambientLight; + + [Editable, Serialize("0,0,0,0", true)] + public Color AmbientLight + { + get { return ambientLight; } + set + { + ambientLight = value; +#if CLIENT + lastAmbientLightEditTime = Timing.TotalTime; +#endif + } + } + public override Rectangle Rect { get @@ -501,54 +516,34 @@ namespace Barotrauma rightDelta[i] = WaveSpread * (waveY[i] - waveY[i + 1]); waveVel[i + 1] += rightDelta[i]; } - - for (int i = 1; i < waveY.Length - 1; i++) - { - waveY[i - 1] += leftDelta[i]; - waveY[i + 1] += rightDelta[i]; - } } //make waves propagate through horizontal gaps foreach (Gap gap in ConnectedGaps) { - if (!gap.IsRoomToRoom || !gap.IsHorizontal || gap.Open <= 0.0f) continue; - if (surface > gap.Rect.Y || surface < gap.Rect.Y - gap.Rect.Height) continue; - - Hull hull2 = this == gap.linkedTo[0] as Hull ? (Hull)gap.linkedTo[1] : (Hull)gap.linkedTo[0]; - float otherSurfaceY = hull2.surface; - if (otherSurfaceY > gap.Rect.Y || otherSurfaceY < gap.Rect.Y - gap.Rect.Height) continue; - - float surfaceDiff = (surface - otherSurfaceY) * gap.Open; if (this != gap.linkedTo[0] as Hull) { - //the first hull linked to the gap handles the wave propagation, - //the second just updates the surfaces to the same level - if (surfaceDiff < 32.0f) - { - hull2.waveY[hull2.waveY.Length - 1] = surfaceDiff * 0.5f; - waveY[0] = -surfaceDiff * 0.5f; - } + //let the first linked hull handle the water propagation continue; } + if (!gap.IsRoomToRoom || !gap.IsHorizontal || gap.Open <= 0.0f) { continue; } + if (surface > gap.Rect.Y || surface < gap.Rect.Y - gap.Rect.Height) { continue; } + + Hull hull2 = this == gap.linkedTo[0] as Hull ? (Hull)gap.linkedTo[1] : (Hull)gap.linkedTo[0]; + float otherSurfaceY = hull2.surface; + if (otherSurfaceY > gap.Rect.Y || otherSurfaceY < gap.Rect.Y - gap.Rect.Height) { continue; } + + float surfaceDiff = (surface - otherSurfaceY) * gap.Open; for (int j = 0; j < 2; j++) { - int i = waveY.Length - 1; + rightDelta[waveY.Length - 1] = WaveSpread * (hull2.waveY[0] - waveY[waveY.Length - 1] - surfaceDiff) * 0.5f; + waveVel[waveY.Length - 1] += rightDelta[waveY.Length - 1]; + waveY[waveY.Length - 1] += rightDelta[waveY.Length - 1]; - leftDelta[i] = WaveSpread * (waveY[i] - waveY[i - 1]); - waveVel[i - 1] += leftDelta[i]; - - rightDelta[i] = WaveSpread * (waveY[i] - hull2.waveY[0] + surfaceDiff); - hull2.waveVel[0] += rightDelta[i]; - - i = 0; - - hull2.leftDelta[i] = WaveSpread * (hull2.waveY[i] - waveY[waveY.Length - 1] - surfaceDiff); - waveVel[waveVel.Length - 1] += hull2.leftDelta[i]; - - hull2.rightDelta[i] = WaveSpread * (hull2.waveY[i] - hull2.waveY[i + 1]); - hull2.waveVel[i + 1] += hull2.rightDelta[i]; + hull2.leftDelta[0] = WaveSpread * (waveY[waveY.Length - 1] - hull2.waveY[0] + surfaceDiff) * 0.5f; + hull2.waveVel[0] += hull2.leftDelta[0]; + hull2.waveY[0] += hull2.leftDelta[0]; } if (surfaceDiff < 32.0f) @@ -557,13 +552,19 @@ namespace Barotrauma hull2.waveY[0] = surfaceDiff * 0.5f; waveY[waveY.Length - 1] = -surfaceDiff * 0.5f; } - else + } + + + //apply spread (two iterations) + for (int j = 0; j < 2; j++) + { + for (int i = 1; i < waveY.Length - 1; i++) { - hull2.waveY[0] += rightDelta[waveY.Length - 1]; - waveY[waveY.Length - 1] += hull2.leftDelta[0]; + waveY[i - 1] += leftDelta[i]; + waveY[i + 1] += rightDelta[i]; } } - + if (waterVolume < Volume) { LethalPressure -= 10.0f * deltaTime; @@ -609,37 +610,33 @@ namespace Barotrauma FireSources.Remove(fire); } - private HashSet adjacentHulls = new HashSet(); - public IEnumerable GetConnectedHulls(bool includingThis, int? searchDepth = null) + private readonly HashSet adjacentHulls = new HashSet(); + public IEnumerable GetConnectedHulls(bool includingThis, int? searchDepth = null, bool ignoreClosedGaps = false) { adjacentHulls.Clear(); int startStep = 0; - searchDepth = searchDepth ?? 100; - return GetAdjacentHulls(includingThis, adjacentHulls, ref startStep, searchDepth.Value); + searchDepth ??= 100; + GetAdjacentHulls(adjacentHulls, ref startStep, searchDepth.Value, ignoreClosedGaps); + if (!includingThis) { adjacentHulls.Remove(this); } + return adjacentHulls; } - private HashSet GetAdjacentHulls(bool includingThis, HashSet connectedHulls, ref int step, int searchDepth) + private void GetAdjacentHulls(HashSet connectedHulls, ref int step, int searchDepth, bool ignoreClosedGaps = false) { - if (includingThis) - { - connectedHulls.Add(this); - } - if (step > searchDepth) - { - return connectedHulls; - } + connectedHulls.Add(this); + if (step > searchDepth) { return; } foreach (Gap g in ConnectedGaps) { + if (ignoreClosedGaps && g.Open <= 0.0f) { continue; } for (int i = 0; i < 2 && i < g.linkedTo.Count; i++) { if (g.linkedTo[i] is Hull hull && !connectedHulls.Contains(hull)) { step++; - hull.GetAdjacentHulls(true, connectedHulls, ref step, searchDepth); + hull.GetAdjacentHulls(connectedHulls, ref step, searchDepth, ignoreClosedGaps); } } } - return connectedHulls; } /// @@ -666,7 +663,7 @@ namespace Barotrauma 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)) + if ((!g.ConnectedDoor.IsOpen && !g.ConnectedDoor.IsBroken) || (g.ConnectedDoor.PredictedState.HasValue && !g.ConnectedDoor.PredictedState.Value)) { if (g.ConnectedDoor.OpenState < 0.1f) continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs index 50ca02659..bdefa47e8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs @@ -2,7 +2,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; @@ -55,7 +55,7 @@ namespace Barotrauma DisplayEntities = new List>(); foreach (XElement entityElement in doc.Root.Elements()) { - string identifier = entityElement.GetAttributeString("identifier", ""); + string identifier = entityElement.GetAttributeString("identifier", entityElement.Name.ToString().ToLowerInvariant()); MapEntityPrefab mapEntity = List.FirstOrDefault(p => p.Identifier == identifier); if (mapEntity == null) { @@ -64,9 +64,9 @@ namespace Barotrauma } Rectangle rect = entityElement.GetAttributeRect("rect", Rectangle.Empty); - if (mapEntity != null && !entityElement.GetAttributeBool("hideinassemblypreview", false)) + if (mapEntity != null && !entityElement.Elements().Any(e => e.Name.LocalName.Equals("wire", StringComparison.OrdinalIgnoreCase))) { - DisplayEntities.Add(new Pair(mapEntity, rect)); + if (!entityElement.GetAttributeBool("hideinassemblypreview", false)) { DisplayEntities.Add(new Pair(mapEntity, rect)); } minX = Math.Min(minX, rect.X); minY = Math.Min(minY, rect.Y - rect.Height); maxX = Math.Max(maxX, rect.Right); @@ -74,7 +74,9 @@ namespace Barotrauma } } - Bounds = new Rectangle(minX, minY, maxX - minX, maxY - minY); + Bounds = minX == int.MaxValue ? + new Rectangle(0, 0, 1, 1) : + new Rectangle(minX, minY, maxX - minX, maxY - minY); Prefabs.Add(this, false); } @@ -89,7 +91,7 @@ namespace Barotrauma CreateInstance(rect.Location.ToVector2(), Submarine.MainSub); } - public List CreateInstance(Vector2 position, Submarine sub) + public List CreateInstance(Vector2 position, Submarine sub, bool selectPrefabs = false) { List entities = MapEntity.LoadAll(sub, configElement, FilePath); if (entities.Count == 0) return entities; @@ -107,10 +109,10 @@ namespace Barotrauma MapEntity.MapLoaded(entities, true); #if CLIENT - if (Screen.Selected == GameMain.SubEditorScreen) + if (Screen.Selected == GameMain.SubEditorScreen && selectPrefabs) { MapEntity.SelectedList.Clear(); - entities.ForEach(e => MapEntity.AddSelection(e)); + entities.ForEach(MapEntity.AddSelection); } #endif return entities; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs index 0870154a1..577b7f3fd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/CaveGenerator.cs @@ -27,19 +27,19 @@ namespace Barotrauma foreach (GraphEdge ge in graphEdges) { - if (Vector2.DistanceSquared(ge.Point1, ge.Point2) < 0.001f) continue; + if (Vector2.DistanceSquared(ge.Point1, ge.Point2) < 0.001f) { continue; } for (int i = 0; i < 2; i++) { Site site = (i == 0) ? ge.Site1 : ge.Site2; - int x = (int)(Math.Floor((site.Coord.X-borders.X) / gridCellSize)); - int y = (int)(Math.Floor((site.Coord.Y-borders.Y) / gridCellSize)); + int x = (int)(Math.Floor((site.Coord.X - borders.X) / gridCellSize)); + int y = (int)(Math.Floor((site.Coord.Y - borders.Y) / gridCellSize)); - x = MathHelper.Clamp(x, 0, cellGrid.GetLength(0)-1); - y = MathHelper.Clamp(y, 0, cellGrid.GetLength(1)-1); - - VoronoiCell cell = cellGrid[x,y].Find(c => c.Site == site); + x = MathHelper.Clamp(x, 0, cellGrid.GetLength(0) - 1); + y = MathHelper.Clamp(y, 0, cellGrid.GetLength(1) - 1); + + VoronoiCell cell = cellGrid[x, y].Find(c => c.Site == site); if (cell == null) { @@ -60,14 +60,62 @@ namespace Barotrauma } } + //add edges to the borders of the graph + foreach (var cell in cells) + { + Vector2? point1 = null, point2 = null; + foreach (GraphEdge ge in cell.Edges) + { + if (MathUtils.NearlyEqual(ge.Point1.X, borders.X) || MathUtils.NearlyEqual(ge.Point1.X, borders.Right) || + MathUtils.NearlyEqual(ge.Point1.Y, borders.Y) || MathUtils.NearlyEqual(ge.Point1.Y, borders.Bottom)) + { + if (point1 == null) + { + point1 = ge.Point1; + } + else if (point2 == null) + { + if (MathUtils.NearlyEqual(point1.Value, ge.Point1)) { continue; } + point2 = ge.Point1; + } + } + if (MathUtils.NearlyEqual(ge.Point2.X, borders.X) || MathUtils.NearlyEqual(ge.Point2.X, borders.Right) || + MathUtils.NearlyEqual(ge.Point2.Y, borders.Y) || MathUtils.NearlyEqual(ge.Point2.Y, borders.Bottom)) + { + if (point1 == null) + { + point1 = ge.Point2; + } + else + { + if (MathUtils.NearlyEqual(point1.Value, ge.Point2)) { continue; } + point2 = ge.Point2; + } + } + if (point1.HasValue && point2.HasValue) + { + Debug.Assert(point1 != point2); + var newEdge = new GraphEdge(point1.Value, point2.Value) + { + Cell1 = cell, + IsSolid = true, + Site1 = cell.Site, + OutsideLevel = true + }; + cell.Edges.Add(newEdge); + break; + } + } + } + return cells; } private static Vector2 GetEdgeNormal(GraphEdge edge, VoronoiCell cell = null) { - if (cell == null) cell = edge.AdjacentCell(null); - if (cell == null) return Vector2.UnitX; + if (cell == null) { cell = edge.AdjacentCell(null); } + if (cell == null) { return Vector2.UnitX; } CompareCCW compare = new CompareCCW(cell.Center); if (compare.Compare(edge.Point1, edge.Point2) == -1) @@ -77,9 +125,7 @@ namespace Barotrauma edge.Point2 = temp; } - Vector2 normal = Vector2.Zero; - - normal = Vector2.Normalize(edge.Point2 - edge.Point1); + Vector2 normal = Vector2.Normalize(edge.Point2 - edge.Point1); Vector2 diffToCell = Vector2.Normalize(cell.Center - edge.Point2); normal = new Vector2(-normal.Y, normal.X); @@ -148,7 +194,7 @@ namespace Barotrauma foreach (GraphEdge edge in currentCell.Edges) { var adjacentCell = edge.AdjacentCell(currentCell); - if (limits.Contains(adjacentCell.Site.Coord.X, adjacentCell.Site.Coord.Y)) + if (adjacentCell != null && limits.Contains(adjacentCell.Site.Coord.X, adjacentCell.Site.Coord.Y)) { allowedEdges.Add(edge); } @@ -161,6 +207,7 @@ namespace Barotrauma for (int i = 0; i < currentCell.Edges.Count; i++) { var adjacentCell = currentCell.Edges[i].AdjacentCell(currentCell); + if (adjacentCell == null) { continue; } double dist = MathUtils.Distance( adjacentCell.Site.Coord.X, adjacentCell.Site.Coord.Y, targetCells[currentTargetIndex].Site.Coord.X, targetCells[currentTargetIndex].Site.Coord.Y); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 6b556debe..05648b653 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -18,6 +18,10 @@ namespace Barotrauma //all entities are disabled after they reach this depth public const int MaxEntityDepth = -300000; public const float ShaftHeight = 1000.0f; + /// + /// The level generator won't try to adjust the width of the main path above this limit. + /// + public const int MaxSubmarineWidth = 16000; public static Level Loaded { @@ -141,8 +145,6 @@ namespace Barotrauma get { return positionsOfInterest; } } - public readonly List UsedPositions = new List(); - public Submarine StartOutpost { get; private set; } public Submarine EndOutpost { get; private set; } @@ -166,6 +168,11 @@ namespace Barotrauma private set; } + public List EntitiesBeforeGenerate { get; private set; } = new List(); + public int EntityCountBeforeGenerate { get; private set; } + public int EntityCountAfterGenerate { get; private set; } + + public float Difficulty { get; @@ -281,8 +288,11 @@ namespace Barotrauma public void Generate(bool mirror) { - if (loaded != null) loaded.Remove(); + if (loaded != null) { loaded.Remove(); } loaded = this; + + EntitiesBeforeGenerate = GetEntityList(); + EntityCountBeforeGenerate = EntitiesBeforeGenerate.Count(); levelObjectManager = new LevelObjectManager(); @@ -316,26 +326,25 @@ namespace Barotrauma SeaFloorTopPos = generationParams.SeaFloorDepth + generationParams.MountainHeightMax + generationParams.SeaFloorVariance; int minWidth = 6500; - int maxWidth = 50000; if (Submarine.MainSub != null) { Rectangle dockedSubBorders = Submarine.MainSub.GetDockedBorders(); - dockedSubBorders.Inflate(dockedSubBorders.Size.ToVector2() * 0.05f); + dockedSubBorders.Inflate(dockedSubBorders.Size.ToVector2() * 0.15f); minWidth = Math.Max(minWidth, Math.Max(dockedSubBorders.Width, dockedSubBorders.Height)); - minWidth = Math.Min(minWidth, maxWidth); + minWidth = Math.Min(minWidth, MaxSubmarineWidth); } Rectangle pathBorders = borders; - pathBorders.Inflate(-minWidth * 2, -minWidth); + pathBorders.Inflate(-Math.Min(minWidth * 2, MaxSubmarineWidth), -minWidth); Debug.Assert(pathBorders.Width > 0 && pathBorders.Height > 0, "The size of the level's path area was negative."); startPosition = new Point( - Rand.Range(minWidth, minWidth * 2, Rand.RandSync.Server), + minWidth, Rand.Range(borders.Height / 2, borders.Height - minWidth * 2, Rand.RandSync.Server)); endPosition = new Point( - borders.Width - Rand.Range(minWidth, minWidth * 2, Rand.RandSync.Server), + borders.Width - minWidth, Rand.Range(borders.Height / 2, borders.Height - minWidth * 2, Rand.RandSync.Server)); //---------------------------------------------------------------------------------- @@ -348,19 +357,25 @@ namespace Barotrauma Point nodeInterval = generationParams.MainPathNodeIntervalRange; for (int x = startPosition.X + nodeInterval.X; - x < endPosition.X - nodeInterval.X; + x < endPosition.X - nodeInterval.X; x += Rand.Range(nodeInterval.X, nodeInterval.Y, Rand.RandSync.Server)) { pathNodes.Add(new Point(x, Rand.Range(pathBorders.Y, pathBorders.Bottom, Rand.RandSync.Server))); } - pathNodes.Add(new Point(endPosition.X, borders.Height)); - - if (pathNodes.Count <= 2) + if (pathNodes.Count == 1) { - pathNodes.Insert(1, borders.Center); + pathNodes.Add(new Point(pathBorders.Center.X, pathBorders.Y)); + } + //if all nodes ended up high up in the level, move one down to make sure we utilize the full height of the level + else if (pathNodes.GetRange(1, pathNodes.Count - 1).All(p => p.Y > pathBorders.Y + pathBorders.Height * 0.25f)) + { + int nodeIndex = Rand.Range(1, pathNodes.Count, Rand.RandSync.Server); + pathNodes[nodeIndex] = new Point(pathNodes[nodeIndex].X, pathBorders.Y); } + pathNodes.Add(new Point(endPosition.X, borders.Height)); + GenerateTunnels(pathNodes, minWidth); //---------------------------------------------------------------------------------- @@ -539,21 +554,21 @@ namespace Barotrauma { foreach (GraphEdge edge in cell.Edges) { - if (mirroredEdges.Contains(edge)) continue; + if (mirroredEdges.Contains(edge)) { continue; } edge.Point1.X = borders.Width - edge.Point1.X; edge.Point2.X = borders.Width - edge.Point2.X; - if (!mirroredSites.Contains(edge.Site1)) + if (edge.Site1 != null && !mirroredSites.Contains(edge.Site1)) { //make sure that sites right at the edge of a grid cell end up in the same cell as in the non-mirrored level if (edge.Site1.Coord.X % GridCellSize < 1.0f && - edge.Site1.Coord.X % GridCellSize >= 0.0f) edge.Site1.Coord.X += 1.0f; + edge.Site1.Coord.X % GridCellSize >= 0.0f) { edge.Site1.Coord.X += 1.0f; } edge.Site1.Coord.X = borders.Width - edge.Site1.Coord.X; mirroredSites.Add(edge.Site1); } - if (!mirroredSites.Contains(edge.Site2)) + if (edge.Site2 != null && !mirroredSites.Contains(edge.Site2)) { if (edge.Site2.Coord.X % GridCellSize < 1.0f && - edge.Site2.Coord.X % GridCellSize >= 0.0f) edge.Site2.Coord.X += 1.0f; + edge.Site2.Coord.X % GridCellSize >= 0.0f) { edge.Site2.Coord.X += 1.0f; } edge.Site2.Coord.X = borders.Width - edge.Site2.Coord.X; mirroredSites.Add(edge.Site2); } @@ -759,6 +774,8 @@ namespace Barotrauma DebugConsole.NewMessage("Generated level with the seed " + seed + " (type: " + generationParams.Name + ")", Color.White); } + EntityCountAfterGenerate = Entity.GetEntityList().Count(); + //assign an ID to make entity events work ID = FindFreeID(); } @@ -1531,6 +1548,13 @@ namespace Barotrauma return tempCells; } + public string GetWreckIDTag(string originalTag, Submarine wreck) + { + string shortSeed = ToolBox.StringToInt(seed + wreck.Info.Name).ToString(); + if (shortSeed.Length > 6) { shortSeed = shortSeed.Substring(0, 6); } + return originalTag + "_" + shortSeed; + } + // For debugging private readonly Dictionary> wreckPositions = new Dictionary>(); private readonly Dictionary> blockedRects = new Dictionary>(); @@ -1546,6 +1570,7 @@ namespace Barotrauma return; } wreckFiles.Shuffle(Rand.RandSync.Server); + int wreckCount = Math.Min(Loaded.GenerationParams.WreckCount, wreckFiles.Count); // Min distance between a wreck and the start/end/other wreck. float minDistance = Sonar.DefaultSonarRange; @@ -1610,7 +1635,6 @@ namespace Barotrauma Type = SubmarineInfo.SubmarineType.Wreck }; Submarine wreck = new Submarine(info); - //wreck.Load(unloadPrevious: false); wreck.MakeWreck(); tempSW.Stop(); Debug.WriteLine($"Wreck {wreck.Info.Name} loaded in { tempSW.ElapsedMilliseconds.ToString()} (ms)"); @@ -1626,17 +1650,18 @@ namespace Barotrauma hull.WaterVolume = hull.Volume * Rand.Range(Loaded.GenerationParams.WreckFloodingHullMinWaterPercentage, Loaded.GenerationParams.WreckFloodingHullMaxWaterPercentage, Rand.RandSync.Server); } } - if (Rand.Value(Rand.RandSync.Server) <= Loaded.GenerationParams.ThalamusProbability) + // Only spawn thalamus when the wreck has some thalamus items defined. + if (Rand.Value(Rand.RandSync.Server) <= Loaded.GenerationParams.ThalamusProbability && wreck.GetItems(false).Any(i => i.Prefab.Category == MapEntityCategory.Thalamus)) { - if (!wreck.CreateThalamus()) + if (!wreck.CreateWreckAI()) { - DebugConsole.NewMessage($"Failed to create thalamus inside {wreckName}.", Color.Red); - wreck.DisableThalamus(); + DebugConsole.NewMessage($"Failed to create wreck AI inside {wreckName}.", Color.Red); + wreck.DisableWreckAI(); } } else { - wreck.DisableThalamus(); + wreck.DisableWreckAI(); } } else @@ -1959,13 +1984,13 @@ namespace Barotrauma int corpseCount = Rand.Range(Loaded.GenerationParams.MinCorpseCount, Loaded.GenerationParams.MaxCorpseCount); var allSpawnPoints = WayPoint.WayPointList.FindAll(wp => wp.Submarine == wreck && wp.CurrentHull != null); var pathPoints = allSpawnPoints.FindAll(wp => wp.SpawnType == SpawnType.Path); + pathPoints.Shuffle(Rand.RandSync.Unsynced); var corpsePoints = allSpawnPoints.FindAll(wp => wp.SpawnType == SpawnType.Corpse); - var spawnPoints = corpsePoints.Union(pathPoints).ToList(); - spawnPoints.Shuffle(Rand.RandSync.Unsynced); + corpsePoints.Shuffle(Rand.RandSync.Unsynced); int spawnCounter = 0; for (int j = 0; j < corpseCount; j++) { - WayPoint sp = spawnPoints.FirstOrDefault(); + WayPoint sp = corpsePoints.FirstOrDefault() ?? pathPoints.FirstOrDefault(); JobPrefab job = sp?.AssignedJob; CorpsePrefab selectedPrefab; if (job == null) @@ -1979,18 +2004,19 @@ namespace Barotrauma selectedPrefab = GetCorpsePrefab(p => p.SpawnPosition == PositionType.Wreck && (p.Job == "any" || p.Job == job.Identifier)); if (selectedPrefab == null) { - spawnPoints.Remove(sp); - sp = spawnPoints.FirstOrDefault(sp => sp.AssignedJob == null); + corpsePoints.Remove(sp); + pathPoints.Remove(sp); + sp = corpsePoints.FirstOrDefault(sp => sp.AssignedJob == null) ?? pathPoints.FirstOrDefault(sp => sp.AssignedJob == null); // Deduce the job from the selected prefab selectedPrefab = GetCorpsePrefab(p => p.SpawnPosition == PositionType.Wreck); job = GetJobPrefab(); } } if (selectedPrefab == null) { continue; } - Vector2 pos; + Vector2 worldPos; if (sp == null) { - if (!TryGetExtraSpawnPoint(out pos)) + if (!TryGetExtraSpawnPoint(out worldPos)) { break; } @@ -1998,15 +2024,17 @@ namespace Barotrauma } else { - pos = sp.WorldPosition; - spawnPoints.Remove(sp); + worldPos = sp.WorldPosition; + corpsePoints.Remove(sp); + pathPoints.Remove(sp); } if (job == null) { continue; } var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: job); - var corpse = Character.Create(CharacterPrefab.HumanConfigFile, pos, ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true); + var corpse = Character.Create(CharacterPrefab.HumanConfigFile, worldPos, ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true); + corpse.AnimController.FindHull(worldPos, true); corpse.TeamID = Character.TeamType.None; corpse.EnableDespawn = false; - selectedPrefab.GiveItems(corpse); + selectedPrefab.GiveItems(corpse, wreck); corpse.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null, log: false); spawnCounter++; @@ -2018,9 +2046,9 @@ namespace Barotrauma JobPrefab GetJobPrefab() => selectedPrefab.Job != null && selectedPrefab.Job != "any" ? JobPrefab.Get(selectedPrefab.Job) : JobPrefab.Random(); } - +#if DEBUG DebugConsole.NewMessage($"{spawnCounter}/{corpseCount} corpses spawned in {wreck.Info.Name}.", spawnCounter == corpseCount ? Color.Green : Color.Yellow); - +#endif bool TryGetExtraSpawnPoint(out Vector2 point) { point = Vector2.Zero; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs index 64fd8ffc9..bf7440d9f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs @@ -238,7 +238,8 @@ namespace Barotrauma } - [Editable, Serialize("5000, 10000", true, description: "The distance between the nodes that are used to generate the main path through the level (min, max). Larger values produce a straighter path.")] + [Editable(VectorComponentLabels = new string[] { "editable.minvalue", "editable.maxvalue" }), + Serialize("5000, 10000", true, description: "The distance between the nodes that are used to generate the main path through the level (min, max). Larger values produce a straighter path.")] public Point MainPathNodeIntervalRange { get { return mainPathNodeIntervalRange; } @@ -256,7 +257,8 @@ namespace Barotrauma set { smallTunnelCount = MathHelper.Clamp(value, 0, 100); } } - [Editable, Serialize("5000, 10000", true, description: "The minimum and maximum length of small tunnels placed along the main path.")] + [Editable(VectorComponentLabels = new string[] { "editable.minvalue", "editable.maxvalue" }), + Serialize("5000, 10000", true, description: "The minimum and maximum length of small tunnels placed along the main path.")] public Point SmallTunnelLengthRange { get { return smallTunnelLengthRange; } @@ -349,8 +351,7 @@ namespace Barotrauma [Serialize(5, true, description: "The maximum number of corpses per wreck."), Editable(MinValueInt = 0, MaxValueInt = 20)] public int MaxCorpseCount { get; set; } - // TODO: default to 0 - [Serialize(1f, true, description: "How likely is it that a Thalamus inhabits a wreck. Percentage from 0 to 1 per wreck."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] + [Serialize(0.0f, true, description: "How likely is it that a Thalamus inhabits a wreck. Percentage from 0 to 1 per wreck."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] public float ThalamusProbability { get; set; } [Serialize(0.5f, true, description: "How likely the water level of a hull inside a wreck is randomly set."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs index ceebafd2c..951521cbc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs @@ -358,19 +358,23 @@ namespace Barotrauma return; } - //check if there are any other contacts with the entity + //check if there are contacts with any other fixture of the trigger //(the OnSeparation callback happens when two fixtures separate, //e.g. if a body stops touching the circular fixture at the end of a capsule-shaped body) ContactEdge contactEdge = fixtureA.Body.ContactList; while (contactEdge != null) { if (contactEdge.Contact != null && + contactEdge.Contact.Enabled && contactEdge.Contact.IsTouching) { - var otherEntity = GetEntity(contactEdge.Contact.FixtureB == fixtureB ? - contactEdge.Contact.FixtureB : - contactEdge.Contact.FixtureA); - if (otherEntity == entity) return; + if (contactEdge.Contact.FixtureA != fixtureA && contactEdge.Contact.FixtureB != fixtureA) + { + var otherEntity = GetEntity(contactEdge.Contact.FixtureB == fixtureB ? + contactEdge.Contact.FixtureB : + contactEdge.Contact.FixtureA); + if (otherEntity == entity) { return; } + } } contactEdge = contactEdge.Next; } @@ -418,10 +422,20 @@ namespace Barotrauma public void Update(float deltaTime) { - if (ParentTrigger != null && !ParentTrigger.IsTriggered) return; + if (ParentTrigger != null && !ParentTrigger.IsTriggered) { return; } triggerers.RemoveWhere(t => t.Removed); + if (physicsBody != null) + { + //failsafe to ensure triggerers get removed when they're far from the trigger + float maxExtent = Math.Max(ConvertUnits.ToDisplayUnits(physicsBody.GetMaxExtent() * 5), 5000.0f); + triggerers.RemoveWhere(t => + { + return Vector2.Distance(t.WorldPosition, WorldPosition) > maxExtent; + }); + } + bool isNotClient = true; #if CLIENT isNotClient = GameMain.Client == null; @@ -511,6 +525,7 @@ namespace Barotrauma ApplyForce(character.AnimController.Collider, deltaTime); foreach (Limb limb in character.AnimController.Limbs) { + if (limb.IsSevered) { continue; } ApplyForce(limb.body, deltaTime); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs index 4eb5b637f..c5445756e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs @@ -1,8 +1,8 @@ -using Microsoft.Xna.Framework; +using Barotrauma.IO; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Xml; using System.Xml.Linq; namespace Barotrauma.RuinGeneration @@ -174,7 +174,7 @@ namespace Barotrauma.RuinGeneration public static void SaveAll() { - XmlWriterSettings settings = new XmlWriterSettings + System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { Indent = true, NewLineOnAttributes = true diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs index 2a96202f1..4b6b241da 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerator.cs @@ -654,13 +654,14 @@ namespace Barotrauma.RuinGeneration { connectionPanel.Locked = true; connectionPanel.CanBeSelected = false; + connectionPanel.Item.ShouldBeSaved = false; } - - // Hide wires for now + // Hide wires if (ic is Wire wire) { wire.Hidden = true; wire.CanBeSelected = false; + wire.Item.ShouldBeSaved = false; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs index ac570b18d..422f731bb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs @@ -19,18 +19,17 @@ namespace Barotrauma //Prefabs.Remove(this); } - public readonly Submarine mainSub; + public readonly SubmarineInfo subInfo; - public LinkedSubmarinePrefab(Submarine submarine) + public LinkedSubmarinePrefab(SubmarineInfo subInfo) { - this.mainSub = submarine; + this.subInfo = subInfo; } protected override void CreateInstance(Rectangle rect) { System.Diagnostics.Debug.Assert(Submarine.MainSub != null); - - LinkedSubmarine.CreateDummy(Submarine.MainSub, mainSub.Info.FilePath, rect.Location.ToVector2()); + LinkedSubmarine.CreateDummy(Submarine.MainSub, subInfo.FilePath, rect.Location.ToVector2()); } } @@ -113,7 +112,9 @@ namespace Barotrauma (int)sl.wallVertices.Max(v => v.X + position.X), (int)sl.wallVertices.Min(v => v.Y + position.Y)); - sl.Rect = new Rectangle(sl.rect.X, sl.rect.Y, sl.rect.Width - sl.rect.X, sl.rect.Y - sl.rect.Height); + int width = sl.rect.Width - sl.rect.X; + int height = sl.rect.Y - sl.rect.Height; + sl.Rect = new Rectangle((int)(position.X - width / 2), (int)(position.Y + height / 2), width, height); } else { @@ -162,8 +163,7 @@ namespace Barotrauma public static LinkedSubmarine Load(XElement element, Submarine submarine) { Vector2 pos = element.GetAttributeVector2("pos", Vector2.Zero); - LinkedSubmarine linkedSub = null; - + LinkedSubmarine linkedSub; if (Screen.Selected == GameMain.SubEditorScreen) { linkedSub = CreateDummy(submarine, element, pos); @@ -198,21 +198,26 @@ namespace Barotrauma for (int i = 0; i < linkedToIds.Length; i++) { linkedSub.linkedToID.Add((ushort)linkedToIds[i]); - if (Screen.Selected == GameMain.SubEditorScreen) - { - if (FindEntityByID((ushort)linkedToIds[i]) is MapEntity linked) - { - linkedSub.linkedTo.Add(linked); - } - } } linkedSub.originalLinkedToID = (ushort)element.GetAttributeInt("originallinkedto", 0); linkedSub.originalMyPortID = (ushort)element.GetAttributeInt("originalmyport", 0); - return linkedSub.loadSub ? linkedSub : null; } + public void LinkDummyToMainSubmarine() + { + if (Screen.Selected != GameMain.SubEditorScreen) { return; } + for (int i = 0; i < linkedToID.Count; i++) + { + if (FindEntityByID(linkedToID[i]) is MapEntity linked) + { + linkedTo.Add(linked); + } + } + } + + public override void OnMapLoaded() { if (!loadSub) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs index 458abde12..4463d9c06 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Md5Hash.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Md5Hash.cs index 1deaed05e..e5dc0990c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Md5Hash.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Md5Hash.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/RoundEndCinematic.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/RoundEndCinematic.cs index 6ea887a58..0964e41bc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/RoundEndCinematic.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/RoundEndCinematic.cs @@ -56,6 +56,15 @@ namespace Barotrauma Level.Loaded.TopBarrier.Enabled = false; + foreach (Character character in Character.CharacterList) + { + character.AnimController.Frozen = true; + foreach (Limb limb in character.AnimController.Limbs) + { + limb.body.PhysEnabled = false; + } + } + cam.TargetPos = Vector2.Zero; float timer = 0.0f; float initialZoom = cam.Zoom; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index aa972af0e..05dd06837 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -283,6 +283,13 @@ namespace Barotrauma } } + [Serialize(false, true), Editable] + public bool NoAITarget + { + get; + private set; + } + public Dictionary SerializableProperties { get; @@ -354,6 +361,7 @@ namespace Barotrauma } StairDirection = Prefab.StairDirection; + NoAITarget = Prefab.NoAITarget; SerializableProperties = SerializableProperty.GetProperties(this); InitProjSpecific(); @@ -381,7 +389,7 @@ namespace Barotrauma } // Only add ai targets automatically to submarine/outpost walls - if (aiTarget == null && HasBody && Tags.Contains("wall") && submarine != null && !submarine.Info.IsWreck && !Prefab.NoAITarget) + if (aiTarget == null && HasBody && Tags.Contains("wall") && submarine != null && !submarine.Info.IsWreck && !NoAITarget) { aiTarget = new AITarget(this) { @@ -818,7 +826,10 @@ namespace Barotrauma Vector2 sectionPos = new Vector2( Sections[sectionIndex].rect.X + Sections[sectionIndex].rect.Width / 2.0f, Sections[sectionIndex].rect.Y - Sections[sectionIndex].rect.Height / 2.0f); - if (world && Submarine != null) sectionPos += Submarine.Position; + if (world && Submarine != null) + { + sectionPos += Submarine.Position; + } return sectionPos; } else @@ -839,7 +850,10 @@ namespace Barotrauma (float)Math.Cos(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation), (float)Math.Sin(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation)) * diffFromCenter; - if (world && Submarine != null) sectionPos += Submarine.Position; + if (world && Submarine != null) + { + sectionPos += Submarine.Position; + } return sectionPos; } @@ -913,7 +927,7 @@ namespace Barotrauma //the structure doesn't have any other gap, log the structure being fixed if (noGaps && attacker != null) { - GameServer.Log((Sections[sectionIndex].gap.IsRoomToRoom ? "Inner" : "Outer") + " wall repaired by " + attacker.Name, ServerLog.MessageType.ItemInteraction); + GameServer.Log((Sections[sectionIndex].gap.IsRoomToRoom ? "Inner" : "Outer") + " wall repaired by " + GameServer.CharacterLogName(attacker), ServerLog.MessageType.ItemInteraction); } #endif DebugConsole.Log("Removing gap (ID " + Sections[sectionIndex].gap.ID + ", section: " + sectionIndex + ") from wall " + ID); @@ -987,11 +1001,11 @@ namespace Barotrauma //the structure didn't have any other gaps yet, log the breach if (noGaps && attacker != null) { - GameServer.Log((Sections[sectionIndex].gap.IsRoomToRoom ? "Inner" : "Outer") + " wall breached by " + attacker.Name, ServerLog.MessageType.ItemInteraction); + GameServer.Log((Sections[sectionIndex].gap.IsRoomToRoom ? "Inner" : "Outer") + " wall breached by " + GameServer.CharacterLogName(attacker), ServerLog.MessageType.ItemInteraction); } #endif } - + float gapOpen = (damage / Prefab.Health - LeakThreshold) * (1.0f / (1.0f - LeakThreshold)); Sections[sectionIndex].gap.Open = gapOpen; } @@ -1255,6 +1269,11 @@ namespace Barotrauma s.UseDropShadow = prefab.Body; } + if (element.Attribute("noaitarget") == null) + { + s.NoAITarget = prefab.NoAITarget; + } + return s; } @@ -1327,6 +1346,7 @@ namespace Barotrauma SerializableProperties = SerializableProperty.DeserializeProperties(this, Prefab.ConfigElement); Sprite.ReloadXML(); SpriteDepth = Sprite.Depth; + NoAITarget = Prefab.NoAITarget; } public override void Update(float deltaTime, Camera cam) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs index 51df613ad..7a828eaf5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs @@ -4,7 +4,7 @@ using System; using System.Linq; using System.Collections.Generic; using System.Xml.Linq; -using System.IO; +using Barotrauma.IO; #if CLIENT using Microsoft.Xna.Framework.Graphics; #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index f05ef3413..7f89167bc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -7,7 +7,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.ComponentModel; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -128,7 +128,7 @@ namespace Barotrauma public PhysicsBody PhysicsBody { - get { return subBody.Body; } + get { return subBody?.Body; } } public Rectangle Borders @@ -178,23 +178,6 @@ namespace Barotrauma } } - private bool? subsLeftBehind; - public bool SubsLeftBehind - { - get - { - if (subsLeftBehind.HasValue) { return subsLeftBehind.Value; } - - CheckSubsLeftBehind(Info.SubmarineElement); - - return subsLeftBehind.Value; - } - //set { subsLeftBehind = value; } - } - public bool LeftBehindSubDockingPortOccupied - { - get; private set; - } public new Vector2 DrawPosition { @@ -222,7 +205,7 @@ namespace Barotrauma public List HullVertices { - get { return subBody.HullVertices; } + get { return subBody?.HullVertices; } } public bool AtDamageDepth @@ -232,7 +215,7 @@ namespace Barotrauma public override string ToString() { - return "Barotrauma.Submarine (" + Info?.Name ?? "[NULL INFO]" + ")"; + return "Barotrauma.Submarine (" + (Info?.Name ?? "[NULL INFO]") + ", " + IdOffset + ")"; } public override bool Removed @@ -249,87 +232,69 @@ namespace Barotrauma ShowSonarMarker = false; PhysicsBody.FarseerBody.BodyType = BodyType.Static; TeamID = Character.TeamType.None; + + string defaultTag = Level.Loaded.GetWreckIDTag("wreck_id", this); + ReplaceIDCardTagRequirements("wreck_id", defaultTag); + + foreach (Item item in Item.ItemList) + { + if (item.Submarine != this) { continue; } + if (item.prefab.Identifier == "idcardwreck" || item.prefab.Identifier == "idcard") + { + foreach (string tag in item.GetTags().ToList()) + { + if (tag == "smallitem") { continue; } + string newTag = Level.Loaded.GetWreckIDTag(tag, this); + item.ReplaceTag(tag, newTag); + ReplaceIDCardTagRequirements(tag, newTag); + } + } + } + + void ReplaceIDCardTagRequirements(string oldTag, string newTag) + { + foreach (Item item in Item.ItemList) + { + if (item.Submarine != this) { continue; } + foreach (ItemComponent ic in item.Components) + { + ReplaceIDCardTagRequirement(ic, RelatedItem.RelationType.Picked, oldTag, newTag); + ReplaceIDCardTagRequirement(ic, RelatedItem.RelationType.Equipped, oldTag, newTag); + } + } + } + + static void ReplaceIDCardTagRequirement(ItemComponent ic, RelatedItem.RelationType relationType, string oldTag, string newTag) + { + if (!ic.requiredItems.ContainsKey(relationType)) { return; } + foreach (RelatedItem requiredItem in ic.requiredItems[relationType]) + { + int index = Array.IndexOf(requiredItem.Identifiers, oldTag); + if (index == -1) { continue; } + requiredItem.Identifiers[index] = newTag; + } + } } - public WreckAI ThalamusAI { get; private set; } - public bool CreateThalamus() + public WreckAI WreckAI { get; private set; } + public bool CreateWreckAI() { MakeWreck(); - var thalamusPrefabs = ItemPrefab.Prefabs.Where(p => p.Category == MapEntityCategory.Thalamus || p.Tags.Contains("thalamus")); - var brainPrefab = thalamusPrefabs.GetRandom(i => i.Tags.Contains("thalamusbrain"), Rand.RandSync.Server); - if (brainPrefab == null) { return false; } - var allItems = GetItems(false); - var thalamusItems = allItems.FindAll(i => i.Prefab.Category == MapEntityCategory.Thalamus || i.HasTag("thalamus")); - var hulls = GetHulls(false); - Item brain = new Item(brainPrefab, Vector2.Zero, this); - Vector2 negativeMargin = new Vector2(40, 20); - Vector2 minSize = brain.Rect.Size.ToVector2() - negativeMargin; - Vector2 maxSize = new Vector2(brain.Rect.Width * 3, brain.Rect.Height * 3); - // First try to get a room that is not too big and not in the edges of the sub. - // Also try not to create the brain in a room that already have carrier items inside. - // Ignore hulls that have any linked hulls to keep the calculations simple. - // Shrink the horizontal axis so that the brain is not placed in the left or right side, where we often have curved walls. - // Also ignore hulls that have open gaps, because we'll want the room to be full of water. The room will be filled with water when the brain is inserted in the room. - Rectangle shrinkedBounds = ToolBox.GetWorldBounds(WorldPosition.ToPoint(), new Point(Borders.Width - 500, Borders.Height)); - bool BaseCondition(Hull h) => h.RectWidth > minSize.X && h.RectHeight > minSize.Y && h.GetLinkedEntities().None() && h.ConnectedGaps.None(g => g.Open > 0); - bool IsNotTooBig(Hull h) => h.RectWidth < maxSize.X && h.RectHeight < maxSize.Y; - bool IsNotInFringes(Hull h) => shrinkedBounds.ContainsWorld(h.WorldRect); - bool DoesNotContainOtherItems(Hull h) => thalamusItems.None(i => i.CurrentHull == h); - Hull brainHull = hulls.GetRandom(h => BaseCondition(h) && IsNotTooBig(h) && IsNotInFringes(h) && DoesNotContainOtherItems(h), Rand.RandSync.Server); - if (brainHull == null) - { - brainHull = hulls.GetRandom(h => BaseCondition(h) && IsNotInFringes(h) && DoesNotContainOtherItems(h), Rand.RandSync.Server); - } - if (brainHull == null) - { - brainHull = hulls.GetRandom(h => BaseCondition(h) && (IsNotInFringes(h) || DoesNotContainOtherItems(h)), Rand.RandSync.Server); - } - if (brainHull == null) - { - brainHull = hulls.GetRandom(BaseCondition, Rand.RandSync.Server); - } - var thalamusStructs = StructurePrefab.Prefabs.Where(p => p.Category == MapEntityCategory.Thalamus); - if (brainHull == null) { return false; } - brainHull.WaterVolume = brainHull.Volume; - brain.SetTransform(brainHull.SimPosition, rotation: 0, findNewHull: false); - brain.CurrentHull = brainHull; - var backgroundPrefab = thalamusStructs.GetRandom(i => i.Tags.Contains("brainroombackground"), Rand.RandSync.Server); - if (backgroundPrefab != null) - { - new Structure(brainHull.Rect, backgroundPrefab, this); - } - var horizontalWallPrefab = thalamusStructs.GetRandom(p => p.Tags.Contains("thalamuswall_horizontal_decorative"), Rand.RandSync.Server); - if (horizontalWallPrefab != null) - { - int height = (int)horizontalWallPrefab.Size.Y; - int halfHeight = height / 2; - int quarterHeight = halfHeight / 2; - new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, this); - new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top - brainHull.Rect.Height + halfHeight + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, this); - } - var verticalWallPrefab = thalamusStructs.GetRandom(p => p.Tags.Contains("thalamuswall_vertical_decorative"), Rand.RandSync.Server); - if (verticalWallPrefab != null) - { - int width = (int)verticalWallPrefab.Size.X; - int halfWidth = width / 2; - int quarterWidth = halfWidth / 2; - new Structure(new Rectangle(brainHull.Rect.Left - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, this); - new Structure(new Rectangle(brainHull.Rect.Right - halfWidth - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, this); - } - ThalamusAI = new WreckAI(this, brain, allItems); - return true; + WreckAI = new WreckAI(this); + return WreckAI != null; } - public void DisableThalamus() + public void DisableWreckAI() { - var thalamusEntities = GetEntities(false, MapEntity.mapEntityList).FindAll(e => e.prefab.Category == MapEntityCategory.Thalamus || e.prefab.Tags.Contains("thalamus")).ToList(); - - foreach (var entity in thalamusEntities) + if (WreckAI == null) { - entity.Remove(); + WreckAI.RemoveThalamusItems(this); + } + else + { + WreckAI?.Remove(); + WreckAI = null; } - ThalamusAI?.Kill(); - ThalamusAI = null; } /// @@ -775,7 +740,7 @@ namespace Barotrauma public void FlipX(List parents = null) { - if (parents == null) parents = new List(); + if (parents == null) { parents = new List(); } parents.Add(this); flippedX = !flippedX; @@ -783,20 +748,25 @@ namespace Barotrauma Item.UpdateHulls(); List bodyItems = Item.ItemList.FindAll(it => it.Submarine == this && it.body != null); - List subEntities = MapEntity.mapEntityList.FindAll(me => me.Submarine == this); foreach (MapEntity e in subEntities) { if (e is Item) continue; - if (e is LinkedSubmarine) + if (e is LinkedSubmarine linkedSub) { - Submarine sub = ((LinkedSubmarine)e).Sub; - if (!parents.Contains(sub)) + Submarine sub = linkedSub.Sub; + if (sub == null) + { + Vector2 relative1 = linkedSub.Position - SubBody.Position; + relative1.X = -relative1.X; + linkedSub.Rect = new Rectangle((relative1 + SubBody.Position).ToPoint(), linkedSub.Rect.Size); + } + else if (!parents.Contains(sub)) { Vector2 relative1 = sub.SubBody.Position - SubBody.Position; relative1.X = -relative1.X; - sub.SetPosition(relative1 + SubBody.Position); + sub.SetPosition(relative1 + SubBody.Position, new List(parents)); sub.FlipX(parents); } } @@ -814,7 +784,7 @@ namespace Barotrauma Vector2 pos = new Vector2(subBody.Position.X, subBody.Position.Y); subBody.Body.Remove(); subBody = new SubmarineBody(this); - SetPosition(pos); + SetPosition(pos, new List(parents.Where(p => p != this))); if (entityGrid != null) { @@ -853,14 +823,15 @@ namespace Barotrauma { //if (PlayerInput.KeyHit(InputType.Crouch) && (this == MainSub)) FlipX(); - if (Level.Loaded == null || subBody == null) { return; } - - if (Info.Type == SubmarineInfo.SubmarineType.Wreck) + if (Info.IsWreck) { - ThalamusAI?.Update(deltaTime); + WreckAI?.Update(deltaTime); } - if (WorldPosition.Y < Level.MaxEntityDepth && + if (subBody?.Body == null) { return; } + + if (Level.Loaded != null && + WorldPosition.Y < Level.MaxEntityDepth && subBody.Body.Enabled && (GameMain.NetworkMember?.RespawnManager == null || this != GameMain.NetworkMember.RespawnManager.RespawnShuttle)) { @@ -887,17 +858,17 @@ namespace Barotrauma return; } + subBody.Body.LinearVelocity = new Vector2( LockX ? 0.0f : subBody.Body.LinearVelocity.X, LockY ? 0.0f : subBody.Body.LinearVelocity.Y); - subBody.Update(deltaTime); for (int i = 0; i < 2; i++) { - if (MainSubs[i] == null) continue; - if (this != MainSubs[i] && MainSubs[i].DockedTo.Contains(this)) return; + if (MainSubs[i] == null) { continue; } + if (this != MainSubs[i] && MainSubs[i].DockedTo.Contains(this)) { return; } } //send updates more frequently if moving fast @@ -919,9 +890,9 @@ namespace Barotrauma prevPosition = position; } - public void SetPosition(Vector2 position, List checkd=null) + public void SetPosition(Vector2 position, List checkd = null) { - if (!MathUtils.IsValid(position)) return; + if (!MathUtils.IsValid(position)) { return; } if (checkd == null) { checkd = new List(); } if (checkd.Contains(this)) { return; } @@ -933,9 +904,16 @@ namespace Barotrauma foreach (Submarine dockedSub in DockedTo) { + if (dockedSub.PhysicsBody.BodyType == BodyType.Static) + { + if (ConnectedDockingPorts.TryGetValue(dockedSub, out DockingPort port)) + { + port.Undock(); + continue; + } + } Vector2? expectedLocation = CalculateDockOffset(this, dockedSub); if (expectedLocation == null) { continue; } - dockedSub.SetPosition(position + expectedLocation.Value, checkd); dockedSub.UpdateTransform(interpolate: false); } @@ -992,6 +970,11 @@ namespace Barotrauma return list.FindAll(e => IsEntityFoundOnThisSub(e, includingConnectedSubs)); } + public IEnumerable GetEntities(bool includingConnectedSubs, IEnumerable list) where T : MapEntity + { + return list.Where(e => IsEntityFoundOnThisSub(e, includingConnectedSubs)); + } + public bool IsEntityFoundOnThisSub(MapEntity entity, bool includingConnectedSubs) { if (entity == null) { return false; } @@ -999,7 +982,7 @@ namespace Barotrauma if (entity.Submarine == null) { return false; } if (includingConnectedSubs) { - return GetConnectedSubs().Any(s => s == entity.Submarine && entity.Submarine.TeamID == TeamID); + return GetConnectedSubs().Any(s => s == entity.Submarine && entity.Submarine.TeamID == TeamID && entity.Submarine.Info.Type == Info.Type); } return false; } @@ -1182,6 +1165,13 @@ namespace Barotrauma Loading = false; MapEntity.MapLoaded(newEntities, true); + foreach (MapEntity me in MapEntity.mapEntityList) + { + if (me is LinkedSubmarine linkedSub && linkedSub.Submarine == this) + { + linkedSub.LinkDummyToMainSubmarine(); + } + } foreach (Hull hull in matchingHulls) { @@ -1221,28 +1211,6 @@ namespace Barotrauma return sub; } - public void CheckSubsLeftBehind(XElement element = null) - { - if (element == null) { element = Info.SubmarineElement; } - - subsLeftBehind = false; - LeftBehindSubDockingPortOccupied = false; - foreach (XElement subElement in element.Elements()) - { - if (!subElement.Name.ToString().Equals("linkedsubmarine")) { continue; } - if (subElement.Attribute("location") == null) { continue; } - - subsLeftBehind = true; - ushort targetDockingPortID = (ushort)subElement.GetAttributeInt("originallinkedto", 0); - XElement targetPortElement = targetDockingPortID == 0 ? null : - element.Elements().FirstOrDefault(e => e.GetAttributeInt("ID", 0) == targetDockingPortID); - if (targetPortElement != null && targetPortElement.GetAttributeIntArray("linked", new int[0]).Length > 0) - { - LeftBehindSubDockingPortOccupied = true; - } - } - } - public void SaveToXElement(XElement element) { element.Add(new XAttribute("name", Info.Name)); @@ -1264,14 +1232,17 @@ namespace Barotrauma e.Save(element); } - CheckSubsLeftBehind(element); + Info.CheckSubsLeftBehind(element); } - public bool SaveAs(string filePath, MemoryStream previewImage = null) + public bool SaveAs(string filePath, System.IO.MemoryStream previewImage = null) { - var newInfo = new SubmarineInfo(this); - newInfo.FilePath = filePath; - newInfo.Name = Path.GetFileNameWithoutExtension(filePath); + var newInfo = new SubmarineInfo(this) + { + GameVersion = GameMain.Version, + FilePath = filePath, + Name = Path.GetFileNameWithoutExtension(filePath) + }; Info.Dispose(); Info = newInfo; return newInfo.SaveAs(filePath, previewImage); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index 1cdcdabb4..d36b529aa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -131,7 +131,7 @@ namespace Barotrauma farseerBody.UserData = this; foreach (Structure wall in Structure.WallList) { - if (wall.Submarine != submarine) continue; + if (wall.Submarine != submarine || wall.IsPlatform) { continue; } Rectangle rect = wall.Rect; @@ -150,7 +150,7 @@ namespace Barotrauma foreach (Hull hull in Hull.hullList) { - if (hull.Submarine != submarine) continue; + if (hull.Submarine != submarine) { continue; } Rectangle rect = hull.Rect; farseerBody.CreateRectangle( @@ -167,7 +167,7 @@ namespace Barotrauma foreach (Item item in Item.ItemList) { - if (item.StaticBodyConfig == null || item.Submarine != submarine) continue; + if (item.StaticBodyConfig == null || item.Submarine != submarine) { continue; } float radius = item.StaticBodyConfig.GetAttributeFloat("radius", 0.0f) * item.Scale; float width = item.StaticBodyConfig.GetAttributeFloat("width", 0.0f) * item.Scale; @@ -180,7 +180,7 @@ namespace Barotrauma if (width > 0.0f && height > 0.0f) { - farseerBody.CreateRectangle(simWidth, simHeight, 5.0f, simPos).UserData = item; + item.StaticFixtures.Add(farseerBody.CreateRectangle(simWidth, simHeight, 5.0f, simPos)); minExtents.X = Math.Min(item.Position.X - width / 2, minExtents.X); minExtents.Y = Math.Min(item.Position.Y - height / 2, minExtents.Y); @@ -189,9 +189,9 @@ namespace Barotrauma } else if (radius > 0.0f && width > 0.0f) { - farseerBody.CreateRectangle(simWidth, simRadius * 2, 5.0f, simPos).UserData = item; - farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitX * simWidth / 2).UserData = item; - farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitX * simWidth / 2).UserData = item; + item.StaticFixtures.Add(farseerBody.CreateRectangle(simWidth, simRadius * 2, 5.0f, simPos)); + item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitX * simWidth / 2)); + item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitX * simWidth / 2)); minExtents.X = Math.Min(item.Position.X - width / 2 - radius, minExtents.X); minExtents.Y = Math.Min(item.Position.Y - radius, minExtents.Y); maxExtents.X = Math.Max(item.Position.X + width / 2 + radius, maxExtents.X); @@ -199,9 +199,9 @@ namespace Barotrauma } else if (radius > 0.0f && height > 0.0f) { - farseerBody.CreateRectangle(simRadius * 2, height, 5.0f, simPos).UserData = item; - farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitY * simHeight / 2).UserData = item; - farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitX * simHeight / 2).UserData = item; + item.StaticFixtures.Add(farseerBody.CreateRectangle(simRadius * 2, height, 5.0f, simPos)); + item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitY * simHeight / 2)); + item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitX * simHeight / 2)); minExtents.X = Math.Min(item.Position.X - radius, minExtents.X); minExtents.Y = Math.Min(item.Position.Y - height / 2 - radius, minExtents.Y); maxExtents.X = Math.Max(item.Position.X + radius, maxExtents.X); @@ -209,12 +209,13 @@ namespace Barotrauma } else if (radius > 0.0f) { - farseerBody.CreateCircle(simRadius, 5.0f, simPos).UserData = item; + item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos)); minExtents.X = Math.Min(item.Position.X - radius, minExtents.X); minExtents.Y = Math.Min(item.Position.Y - radius, minExtents.Y); maxExtents.X = Math.Max(item.Position.X + radius, maxExtents.X); maxExtents.Y = Math.Max(item.Position.Y + radius, maxExtents.Y); } + item.StaticFixtures.ForEach(f => f.UserData = item); } Borders = new Rectangle((int)minExtents.X, (int)maxExtents.Y, (int)(maxExtents.X - minExtents.X), (int)(maxExtents.Y - minExtents.Y)); @@ -301,7 +302,7 @@ namespace Barotrauma //------------------------- //if outside left or right edge of the level - if (Position.X < 0 || Position.X > Level.Loaded.Size.X) + if (Level.Loaded != null && (Position.X < 0 || Position.X > Level.Loaded.Size.X)) { Rectangle worldBorders = Borders; worldBorders.Location += MathUtils.ToPoint(Position); @@ -385,12 +386,13 @@ namespace Barotrauma foreach (Character c in Character.CharacterList) { - if (c.AnimController.CurrentHull != null && c.AnimController.CanEnterSubmarine) continue; + if (c.AnimController.CurrentHull != null && c.AnimController.CanEnterSubmarine) { continue; } foreach (Limb limb in c.AnimController.Limbs) { + if (limb.IsSevered) { continue; } //if the character isn't inside the bounding box, continue - if (!Submarine.RectContains(worldBorders, limb.WorldPosition)) continue; + if (!Submarine.RectContains(worldBorders, limb.WorldPosition)) { continue; } //cast a line from the position of the character to the same direction as the translation of the sub //and see where it intersects with the bounding box @@ -454,11 +456,11 @@ namespace Barotrauma depthDamageTimer -= deltaTime; - if (depthDamageTimer > 0.0f) return; + if (depthDamageTimer > 0.0f) { return; } foreach (Structure wall in Structure.WallList) { - if (wall.Submarine != submarine) continue; + if (wall.Submarine != submarine) { continue; } if (wall.Health < depth * 0.01f) { @@ -498,10 +500,14 @@ namespace Barotrauma } return collision; } - if (f2.Body.UserData is Character character) + else if (f2.Body.UserData is Character character) { return CheckCharacterCollision(contact, character); } + else if (f2.UserData is Items.Components.DockingPort) + { + return false; + } lock (impactQueue) { @@ -692,8 +698,8 @@ namespace Barotrauma //find all contacts between this sub and level walls List levelContacts = new List(); - ContactEdge contactEdge = Body.FarseerBody.ContactList; - while (contactEdge.Next != null) + ContactEdge contactEdge = Body?.FarseerBody?.ContactList; + while (contactEdge?.Next != null) { if (contactEdge.Contact.Enabled && contactEdge.Other.UserData is VoronoiCell && @@ -705,7 +711,7 @@ namespace Barotrauma contactEdge = contactEdge.Next; } - if (levelContacts.Count == 0) return; + if (levelContacts.Count == 0) { return; } //if this sub is in contact with the level, apply artifical impacts //to both subs to prevent the other sub from bouncing on top of this one @@ -794,6 +800,7 @@ namespace Barotrauma foreach (Limb limb in c.AnimController.Limbs) { + if (limb.IsSevered) { continue; } limb.body.ApplyLinearImpulse(limb.Mass * impulse, 10.0f); } c.AnimController.Collider.ApplyLinearImpulse(c.AnimController.Collider.Mass * impulse, 10.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index 5300465af..951c830a5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -2,7 +2,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.ComponentModel; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -68,6 +68,8 @@ namespace Barotrauma public bool IsOutpost => Type == SubmarineType.Outpost; public bool IsWreck => Type == SubmarineType.Wreck; + public bool IsPlayer => Type == SubmarineType.Player; + public enum SubmarineType { Player, Outpost, Wreck } public SubmarineType Type { get; set; } @@ -99,7 +101,11 @@ namespace Barotrauma set; } - public readonly XElement SubmarineElement; + public XElement SubmarineElement + { + get; + private set; + } public override string ToString() { @@ -126,6 +132,22 @@ namespace Barotrauma } } + private bool? subsLeftBehind; + public bool SubsLeftBehind + { + get + { + if (subsLeftBehind.HasValue) { return subsLeftBehind.Value; } + CheckSubsLeftBehind(SubmarineElement); + return subsLeftBehind.Value; + } + } + + public bool LeftBehindSubDockingPortOccupied + { + get; private set; + } + //constructors & generation ---------------------------------------------------- public SubmarineInfo() { @@ -135,7 +157,7 @@ namespace Barotrauma RequiredContentPackages = new HashSet(); } - public SubmarineInfo(string filePath, string hash = "", XElement element = null) + public SubmarineInfo(string filePath, string hash = "", XElement element = null, bool tryLoad = true) { FilePath = filePath; if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath)) @@ -160,41 +182,23 @@ namespace Barotrauma RequiredContentPackages = new HashSet(); - if (element == null) + if (element == null && tryLoad) { - XDocument doc = null; - int maxLoadRetries = 4; - for (int i = 0; i <= maxLoadRetries; i++) - { - doc = OpenFile(filePath, out Exception e); - if (e != null && !(e is IOException)) { break; } - if (doc != null || i == maxLoadRetries || !File.Exists(filePath)) { break; } - DebugConsole.NewMessage("Opening submarine file \"" + filePath + "\" failed, retrying in 250 ms..."); - Thread.Sleep(250); - } - if (doc == null || doc.Root == null) - { - IsFileCorrupted = true; - return; - } - - if (string.IsNullOrWhiteSpace(hash)) - { - StartHashDocTask(doc); - } - - SubmarineElement = doc.Root; + Reload(); } else { SubmarineElement = element; } + Name = SubmarineElement.GetAttributeString("name", null) ?? Name; + Init(); } public SubmarineInfo(Submarine sub) : this(sub.Info) { + GameVersion = GameMain.Version; SubmarineElement = new XElement("Submarine"); sub.SaveToXElement(SubmarineElement); Init(); @@ -222,6 +226,30 @@ namespace Barotrauma #endif } + public void Reload() + { + XDocument doc = null; + int maxLoadRetries = 4; + for (int i = 0; i <= maxLoadRetries; i++) + { + doc = OpenFile(FilePath, out Exception e); + if (e != null && !(e is System.IO.IOException)) { break; } + if (doc != null || i == maxLoadRetries || !File.Exists(FilePath)) { break; } + DebugConsole.NewMessage("Opening submarine file \"" + FilePath + "\" failed, retrying in 250 ms..."); + Thread.Sleep(250); + } + if (doc == null || doc.Root == null) + { + IsFileCorrupted = true; + return; + } + if (hash == null) + { + StartHashDocTask(doc); + } + SubmarineElement = doc.Root; + } + private void Init() { DisplayName = TextManager.Get("Submarine.Name." + Name, true); @@ -317,8 +345,31 @@ namespace Barotrauma Tags &= ~tag; } + public void CheckSubsLeftBehind(XElement element = null) + { + if (element == null) { element = SubmarineElement; } + + subsLeftBehind = false; + LeftBehindSubDockingPortOccupied = false; + foreach (XElement subElement in element.Elements()) + { + if (!subElement.Name.ToString().Equals("linkedsubmarine", StringComparison.OrdinalIgnoreCase)) { continue; } + if (subElement.Attribute("location") == null) { continue; } + + subsLeftBehind = true; + ushort targetDockingPortID = (ushort)subElement.GetAttributeInt("originallinkedto", 0); + XElement targetPortElement = targetDockingPortID == 0 ? null : + element.Elements().FirstOrDefault(e => e.GetAttributeInt("ID", 0) == targetDockingPortID); + if (targetPortElement != null && targetPortElement.GetAttributeIntArray("linked", new int[0]).Length > 0) + { + LeftBehindSubDockingPortOccupied = true; + } + } + } + + //saving/loading ---------------------------------------------------- - public bool SaveAs(string filePath, MemoryStream previewImage=null) + public bool SaveAs(string filePath, System.IO.MemoryStream previewImage=null) { var newElement = new XElement(SubmarineElement.Name, SubmarineElement.Attributes().Where(a => !string.Equals(a.Name.LocalName, "previewimage", StringComparison.InvariantCultureIgnoreCase)), @@ -405,7 +456,11 @@ namespace Barotrauma try { filePaths = Directory.GetFiles(SavePath).ToList(); - subDirectories = Directory.GetDirectories(SavePath); + subDirectories = Directory.GetDirectories(SavePath).Where(s => + { + DirectoryInfo dir = new DirectoryInfo(s); + return (dir.Attributes & System.IO.FileAttributes.Hidden) == 0; + }).ToArray(); } catch (Exception e) { @@ -504,12 +559,12 @@ namespace Barotrauma if (extension == ".sub") { - Stream stream = null; + System.IO.Stream stream = null; try { stream = SaveUtil.DecompressFiletoStream(file); } - catch (FileNotFoundException e) + catch (System.IO.FileNotFoundException e) { exception = e; DebugConsole.ThrowError("Loading submarine \"" + file + "\" failed! (File not found) " + Environment.StackTrace, e); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index c3fe670a4..7cf5bc934 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -124,13 +124,15 @@ namespace Barotrauma #if CLIENT if (iconSprites == null) { - iconSprites = new Dictionary() + iconSprites = new Dictionary() { - { SpawnType.Path, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(0,0,128,128)) }, - { SpawnType.Human, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(128,0,128,128)) }, - { SpawnType.Enemy, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(256,0,128,128)) }, - { SpawnType.Cargo, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(384,0,128,128)) }, - { SpawnType.Corpse, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(512,0,128,128)) } + { "Path", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(0,0,128,128)) }, + { "Human", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(128,0,128,128)) }, + { "Enemy", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(256,0,128,128)) }, + { "Cargo", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(384,0,128,128)) }, + { "Corpse", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(512,0,128,128)) }, + { "Ladder", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(0,128,128,128)) }, + { "Door", new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(128,128,128,128)) } }; } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs index b2bc4db8f..53e2f4bee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs @@ -11,6 +11,8 @@ namespace Barotrauma.Networking Default, Error, Dead, Server, Radio, Private, Console, MessageBox, Order, ServerLog, ServerMessageBox } + public enum PlayerConnectionChangeType { None = 0, Joined = 1, Kicked = 2, Disconnected = 3, Banned = 4 } + partial class ChatMessage { public const int MaxLength = 150; @@ -58,8 +60,10 @@ namespace Barotrauma.Networking } public ChatMessageType Type; + public PlayerConnectionChangeType ChangeType; public readonly Character Sender; + public readonly Client SenderClient; public readonly string SenderName; @@ -89,19 +93,21 @@ namespace Barotrauma.Networking set; } - protected ChatMessage(string senderName, string text, ChatMessageType type, Character sender) + protected ChatMessage(string senderName, string text, ChatMessageType type, Character sender, Client client, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None) { Text = text; Type = type; Sender = sender; + SenderClient = client; SenderName = senderName; + ChangeType = changeType; } - public static ChatMessage Create(string senderName, string text, ChatMessageType type, Character sender) + public static ChatMessage Create(string senderName, string text, ChatMessageType type, Character sender, Client client = null, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None) { - return new ChatMessage(senderName, text, type, sender); + return new ChatMessage(senderName, text, type, sender, client ?? GameMain.NetworkMember?.ConnectedClients?.Find(c => c.Character == sender), changeType); } public static string GetChatMessageCommand(string message, out string messageWithoutCommand) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs index 0ece35d95..7402c316d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; +using Barotrauma.IO; using System.IO.Pipes; using System.Linq; using System.Text; @@ -12,8 +12,8 @@ namespace Barotrauma.Networking { static partial class ChildServerRelay { - private static Stream writeStream; - private static Stream readStream; + private static System.IO.Stream writeStream; + private static System.IO.Stream readStream; private static volatile bool shutDown; public static bool HasShutDown { @@ -233,7 +233,12 @@ namespace Barotrauma.Networking { writeStream?.Write(msg, 0, msg.Length); } - catch (IOException) + catch (ObjectDisposedException) + { + shutDown = true; + break; + } + catch (System.IO.IOException) { shutDown = true; break; @@ -263,7 +268,12 @@ namespace Barotrauma.Networking lengthBytes[1] = (byte)0; writeStream?.Write(lengthBytes, 0, 2); } - catch (IOException) + catch (ObjectDisposedException) + { + shutDown = true; + break; + } + catch (System.IO.IOException) { shutDown = true; break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs index c46241964..16cc90dd4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs @@ -14,6 +14,8 @@ namespace Barotrauma.Networking public byte ID; public UInt64 SteamID; + public UInt16 Ping; + public string PreferredJob; public Character.TeamType TeamID; @@ -86,6 +88,14 @@ namespace Barotrauma.Networking } } + public bool Spectating + { + get + { + return inGame && character == null; + } + } + private bool muted; public bool Muted { @@ -105,6 +115,8 @@ namespace Barotrauma.Networking } } + public bool HasPermissions = false; + public VoipQueue VoipQueue { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs index f84dc043a..d77dc356a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Xml.Linq; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs index 363421c85..a99e369a1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs @@ -125,7 +125,7 @@ namespace Barotrauma { public readonly Entity Entity; - public readonly UInt16 OriginalID; + public readonly UInt16 OriginalID, OriginalInventoryID; public readonly bool Remove = false; @@ -133,6 +133,10 @@ namespace Barotrauma { Entity = entity; OriginalID = entity.ID; + if (entity is Item item && item.ParentInventory?.Owner != null) + { + OriginalInventoryID = item.ParentInventory.Owner.ID; + } Remove = remove; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/KarmaManager.cs index bea21a9b3..b8688b981 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/KarmaManager.cs @@ -1,9 +1,8 @@ using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Text; -using System.Xml; using System.Xml.Linq; namespace Barotrauma @@ -53,8 +52,20 @@ namespace Barotrauma [Serialize(0.25f, true)] public float DamageFriendlyKarmaDecrease { get; set; } + [Serialize(0.25f, true)] + public float StunFriendlyKarmaDecrease { get; set; } + + [Serialize(0.3f, true)] + public float StunFriendlyKarmaDecreaseThreshold { get; set; } + [Serialize(1.0f, true)] public float ExtinguishFireKarmaIncrease { get; set; } + + [Serialize(defaultValue: 15.0f, true)] + public float DangerousItemStealKarmaDecrease { get; set; } + + [Serialize(defaultValue: false, true)] + public bool DangerousItemStealBots { get; set; } private int allowedWireDisconnectionsPerMinute; @@ -104,7 +115,7 @@ namespace Barotrauma doc = XMLExtensions.TryLoadXml(ConfigFile); break; } - catch (IOException) + catch (System.IO.IOException) { if (i == maxLoadRetries) { break; } DebugConsole.NewMessage("Opening karma settings file \"" + ConfigFile + "\" failed, retrying in 250 ms..."); @@ -159,7 +170,7 @@ namespace Barotrauma doc.Root.Add(preset.Value); } - XmlWriterSettings settings = new XmlWriterSettings + System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { Indent = true, NewLineOnAttributes = true @@ -172,11 +183,11 @@ namespace Barotrauma { using (var writer = XmlWriter.Create(ConfigFile, settings)) { - doc.Save(writer); + doc.SaveSafe(writer); } break; } - catch (IOException) + catch (System.IO.IOException) { if (i == maxLoadRetries) { throw; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs index 6fd92e83f..5fca74a45 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs @@ -49,6 +49,26 @@ namespace Barotrauma.Networking public const int MaxEventPacketsPerUpdate = 4; + /// + /// How long the server waits for the clients to get in sync after the round has started before kicking them + /// + public const float RoundStartSyncDuration = 60.0f; + + /// + /// How long the server keeps events that everyone currently synced has received + /// + public const float EventRemovalTime = 15.0f; + + /// + /// If a client hasn't received an event that has been succesfully sent to someone within this time, they get kicked + /// + public const float OldReceivedEventKickTime = 10.0f; + + /// + /// If a client hasn't received an event after this time, they get kicked + /// + public const float OldEventKickTime = 30.0f; + /// /// Interpolates the positional error of a physics body towards zero. /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs index 1cac0fe43..f4a9431ea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs @@ -15,7 +15,8 @@ namespace Barotrauma.Networking ChangeProperty, Control, UpdateSkills, - Combine + Combine, + ExecuteAttack } public readonly Entity Entity; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index 5858d7f55..217df7536 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -19,6 +19,8 @@ namespace Barotrauma.Networking VOICE, + PING_RESPONSE, + RESPONSE_STARTGAME, //tell the server whether you're ready to start SERVER_COMMAND, //tell the server to end a round or kick/ban someone (special permissions required) @@ -60,6 +62,9 @@ namespace Barotrauma.Networking VOICE, + PING_REQUEST, //ping the client + CLIENT_PINGS, //tell the client the pings of all other clients + QUERY_STARTGAME, //ask the clients whether they're ready to start STARTGAME, //start a new round STARTGAMEFINALIZE, //finalize round initialization @@ -212,9 +217,9 @@ namespace Barotrauma.Networking return radioComponent.HasRequiredContainedItems(sender, addMessage: false); } - public void AddChatMessage(string message, ChatMessageType type, string senderName = "", Character senderCharacter = null) + public void AddChatMessage(string message, ChatMessageType type, string senderName = "", Character senderCharacter = null, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None) { - AddChatMessage(ChatMessage.Create(senderName, message, type, senderCharacter)); + AddChatMessage(ChatMessage.Create(senderName, message, type, senderCharacter, changeType: changeType)); } public virtual void AddChatMessage(ChatMessage message) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs index 32e4d483b..76243edbc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs @@ -25,7 +25,7 @@ namespace Barotrauma.Networking } public OrderChatMessage(Order order, string orderOption, string text, Entity targetEntity, Character targetCharacter, Character sender) - : base(sender?.Name, text, ChatMessageType.Order, sender) + : base(sender?.Name, text, ChatMessageType.Order, sender, GameMain.NetworkMember.ConnectedClients.Find(c => c.Character == sender)) { Order = order; OrderOption = orderOption; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs index 8372bc26f..40c7a1104 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs @@ -1,7 +1,7 @@ using Lidgren.Network; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.IO.Compression; using System.Runtime.InteropServices; using System.Text; @@ -519,7 +519,7 @@ namespace Barotrauma.Networking } else { - using (MemoryStream output = new MemoryStream()) + using (System.IO.MemoryStream output = new System.IO.MemoryStream()) { using (DeflateStream dstream = new DeflateStream(output, CompressionLevel.Fastest)) { @@ -613,9 +613,9 @@ namespace Barotrauma.Networking if (isCompressed) { byte[] decompressedData; - using (MemoryStream input = new MemoryStream(inBuf, startPos, inLength)) + using (System.IO.MemoryStream input = new System.IO.MemoryStream(inBuf, startPos, inLength)) { - using (MemoryStream output = new MemoryStream()) + using (System.IO.MemoryStream output = new System.IO.MemoryStream()) { using (DeflateStream dstream = new DeflateStream(input, CompressionMode.Decompress)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs index d656a8baf..a8f02444a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs @@ -1,7 +1,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; namespace Barotrauma.Networking @@ -11,7 +11,9 @@ namespace Barotrauma.Networking private struct LogMessage { public readonly string Text; + public readonly string SanitizedText; public readonly MessageType Type; + public readonly List RichData; public LogMessage(string text, MessageType type) { @@ -23,6 +25,7 @@ namespace Barotrauma.Networking { Text = $"[{DateTime.Now.ToString()}]\n {TextManager.GetServerMessage(text)}"; } + RichData = RichTextData.GetRichTextData(Text, out SanitizedText); Type = type; } @@ -38,6 +41,7 @@ namespace Barotrauma.Networking Wiring, ServerMessage, ConsoleUsage, + Karma, Error, } @@ -51,6 +55,7 @@ namespace Barotrauma.Networking { MessageType.Wiring, new Color(255, 157, 85) }, { MessageType.ServerMessage, new Color(157, 225, 160) }, { MessageType.ConsoleUsage, new Color(0, 162, 232) }, + { MessageType.Karma, new Color(75, 88, 255) }, { MessageType.Error, Color.Red }, }; @@ -64,6 +69,7 @@ namespace Barotrauma.Networking { MessageType.Wiring, "Wiring" }, { MessageType.ServerMessage, "ServerMessage" }, { MessageType.ConsoleUsage, "ConsoleUsage" }, + { MessageType.Karma, "Karma" }, { MessageType.Error, "Error" } }; @@ -101,12 +107,12 @@ namespace Barotrauma.Networking { //string logLine = "[" + DateTime.Now.ToLongTimeString() + "] " + line; + var newText = new LogMessage(line, messageType); + #if SERVER - DebugConsole.NewMessage(line, messageColor[messageType]); //TODO: REMOVE + DebugConsole.NewMessage(newText.SanitizedText, messageColor[messageType]); //TODO: REMOVE #endif - var newText = new LogMessage(line, messageType); - lines.Enqueue(newText); #if CLIENT @@ -134,7 +140,7 @@ namespace Barotrauma.Networking #if CLIENT while (listBox != null && listBox.Content.CountChildren > LinesPerFile) { - listBox.RemoveChild(listBox.Content.Children.First()); + listBox.RemoveChild(reverseOrder ? listBox.Content.Children.First() : listBox.Content.Children.Last()); } #endif } @@ -167,7 +173,7 @@ namespace Barotrauma.Networking try { - File.WriteAllLines(filePath, lines.Select(l => l.Text)); + File.WriteAllLines(filePath, lines.Select(l => l.SanitizedText)); } catch (Exception e) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index 49d1c1ecf..4062779b5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Net; using System.Security.Cryptography; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs index 956f8a3dc..8b45a594c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs @@ -1,6 +1,8 @@ -using System; +using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.InteropServices; #if USE_STEAM namespace Barotrauma.Steam @@ -47,7 +49,7 @@ namespace Barotrauma.Steam private static bool isInitialized; public static bool IsInitialized => isInitialized; - + public static void Initialize() { InitializeProjectSpecific(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voip/VoipQueue.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voip/VoipQueue.cs index 5eddf719b..7c24c7f02 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voip/VoipQueue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voip/VoipQueue.cs @@ -57,6 +57,12 @@ namespace Barotrauma.Networking protected set; } + public bool ForceLocal + { + get; + set; + } + public DateTime LastReadTime { get; @@ -76,7 +82,7 @@ namespace Barotrauma.Networking QueueID = id; CanSend = canSend; CanReceive = canReceive; - LatestBufferID = BUFFER_COUNT-1; + LatestBufferID = BUFFER_COUNT - 1; firstRead = true; LastReadTime = DateTime.Now; @@ -84,7 +90,7 @@ namespace Barotrauma.Networking public void EnqueueBuffer(int length) { - if (length > byte.MaxValue) return; + if (length > byte.MaxValue) { return; } newestBufferInd = (newestBufferInd + 1) % BUFFER_COUNT; @@ -92,17 +98,18 @@ namespace Barotrauma.Networking bufferLengths[newestBufferInd] = length; BufferToQueue.CopyTo(buffers[newestBufferInd], 0); - - if ((enqueuedTotalLength+length)>0) LatestBufferID++; + + if ((enqueuedTotalLength + length) > 0) { LatestBufferID++; } } - public void RetrieveBuffer(int id,out int outSize,out byte[] outBuf) + public void RetrieveBuffer(int id, out int outSize, out byte[] outBuf) { lock (buffers) { if (id >= LatestBufferID - (BUFFER_COUNT - 1) && id <= LatestBufferID) { - int index = (newestBufferInd - (LatestBufferID - id)); if (index < 0) index += BUFFER_COUNT; + int index = newestBufferInd - (LatestBufferID - id); + if (index < 0) { index += BUFFER_COUNT; } outSize = bufferLengths[index]; outBuf = buffers[index]; return; @@ -114,30 +121,39 @@ namespace Barotrauma.Networking public virtual void Write(IWriteMessage msg) { - if (!CanSend) throw new Exception("Called Write on a VoipQueue not set up for sending"); + if (!CanSend) { throw new Exception("Called Write on a VoipQueue not set up for sending"); } msg.Write((UInt16)LatestBufferID); - for (int i = 0; i < BUFFER_COUNT; i++) + msg.Write(ForceLocal); msg.WritePadBits(); + lock (buffers) { - int index = (newestBufferInd + i + 1) % BUFFER_COUNT; + for (int i = 0; i < BUFFER_COUNT; i++) + { + int index = (newestBufferInd + i + 1) % BUFFER_COUNT; - msg.Write((byte)bufferLengths[index]); - msg.Write(buffers[index], 0, bufferLengths[index]); + msg.Write((byte)bufferLengths[index]); + msg.Write(buffers[index], 0, bufferLengths[index]); + } } } - public virtual bool Read(IReadMessage msg) + public virtual bool Read(IReadMessage msg, bool discardData = false) { - if (!CanReceive) throw new Exception("Called Read on a VoipQueue not set up for receiving"); + if (!CanReceive) { throw new Exception("Called Read on a VoipQueue not set up for receiving"); } UInt16 incLatestBufferID = msg.ReadUInt16(); - if (firstRead || NetIdUtils.IdMoreRecent(incLatestBufferID,LatestBufferID)) + if ((firstRead || NetIdUtils.IdMoreRecent(incLatestBufferID, LatestBufferID)) && !discardData) { + ForceLocal = msg.ReadBoolean(); msg.ReadPadBits(); + firstRead = false; - for (int i = 0; i < BUFFER_COUNT; i++) + lock (buffers) { - bufferLengths[i] = msg.ReadByte(); - buffers[i] = msg.ReadBytes(bufferLengths[i]); + for (int i = 0; i < BUFFER_COUNT; i++) + { + bufferLengths[i] = msg.ReadByte(); + buffers[i] = msg.ReadBytes(bufferLengths[i]); + } } newestBufferInd = BUFFER_COUNT - 1; LatestBufferID = incLatestBufferID; @@ -146,6 +162,7 @@ namespace Barotrauma.Networking } else { + msg.ReadBoolean(); msg.ReadPadBits(); for (int i = 0; i < BUFFER_COUNT; i++) { byte len = msg.ReadByte(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/WhiteList.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/WhiteList.cs index d4a02a715..cb197f3b9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/WhiteList.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/WhiteList.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; namespace Barotrauma.Networking diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/Physics.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/Physics.cs index b19aa5e29..2c32ebdff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/Physics.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/Physics.cs @@ -16,7 +16,7 @@ namespace Barotrauma public const Category CollisionLevel = Category.Cat8; public const Category CollisionRepair = Category.Cat9; - public static float DisplayToRealWorldRatio = 1.0f / 80.0f; + public static float DisplayToRealWorldRatio = 1.0f / 100.0f; public const float DisplayToSimRation = 100.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index 974d8e9a1..bed982de1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -88,6 +88,8 @@ namespace Barotrauma Circle, Rectangle, Capsule, HorizontalCapsule }; + public const float DefaultAngularDamping = 5.0f; + private static readonly List list = new List(); public static List List { @@ -342,7 +344,7 @@ namespace Barotrauma FarseerBody.BodyType = BodyType.Dynamic; FarseerBody.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel; FarseerBody.CollisionCategories = Physics.CollisionCharacter; - FarseerBody.AngularDamping = 5.0f; + FarseerBody.AngularDamping = DefaultAngularDamping; FarseerBody.FixedRotation = true; FarseerBody.Friction = 0.05f; FarseerBody.Restitution = 0.05f; @@ -370,15 +372,15 @@ namespace Barotrauma list.Add(this); } - public PhysicsBody(XElement element, Vector2 position, float scale=1.0f) + public PhysicsBody(XElement element, Vector2 position, float scale = 1.0f) { float radius = ConvertUnits.ToSimUnits(element.GetAttributeFloat("radius", 0.0f)) * scale; float height = ConvertUnits.ToSimUnits(element.GetAttributeFloat("height", 0.0f)) * scale; float width = ConvertUnits.ToSimUnits(element.GetAttributeFloat("width", 0.0f)) * scale; density = element.GetAttributeFloat("density", 10.0f); CreateBody(width, height, radius, density); - //Enum.TryParse(element.GetAttributeString("bodytype", "Dynamic"), out BodyType bodyType); - FarseerBody.BodyType = BodyType.Dynamic; + Enum.TryParse(element.GetAttributeString("bodytype", "Dynamic"), out BodyType bodyType); + FarseerBody.BodyType = bodyType; FarseerBody.CollisionCategories = Physics.CollisionItem; FarseerBody.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform; FarseerBody.Friction = element.GetAttributeFloat("friction", 0.3f); @@ -428,7 +430,7 @@ namespace Barotrauma /// For rectangles, the front is either at the top or at the right, depending on which one of the two is greater: width or height. /// The rotation is in radians. /// - public Vector2 GetLocalFront(float spritesheetRotation = 0) + public Vector2 GetLocalFront(float? spritesheetRotation = null) { Vector2 pos; switch (bodyShape) @@ -443,12 +445,12 @@ namespace Barotrauma pos = new Vector2(0.0f, radius); break; case Shape.Rectangle: - pos = new Vector2(0.0f, Math.Max(height, width) / 2.0f); + pos = height > width ? new Vector2(0, height / 2) : new Vector2(width / 2, 0); break; default: throw new NotImplementedException(); } - return spritesheetRotation == 0 ? pos : Vector2.Transform(pos, Matrix.CreateRotationZ(-spritesheetRotation)); + return spritesheetRotation.HasValue ? Vector2.Transform(pos, Matrix.CreateRotationZ(-spritesheetRotation.Value)) : pos; } public float GetMaxExtent() diff --git a/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs b/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs index e3714694e..ec801f1f3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ProcGen/VoronoiElements.cs @@ -168,20 +168,13 @@ namespace Voronoi2 } midPoint /= vertices.Length; - - for (int i = 1; i < vertices.Length; i++ ) + for (int i = 0; i < vertices.Length; i++) { - GraphEdge ge = new GraphEdge(vertices[i-1], vertices[i]); - + GraphEdge ge = new GraphEdge(vertices[i], vertices[MathUtils.PositiveModulo(i + 1, vertices.Length)]); System.Diagnostics.Debug.Assert(ge.Point1 != ge.Point2); - Edges.Add(ge); } - GraphEdge lastEdge = new GraphEdge(vertices[0], vertices[vertices.Length-1]); - - Edges.Add(lastEdge); - Site = new Site(); Site.SetPoint(midPoint); } @@ -198,9 +191,8 @@ namespace Voronoi2 { foreach (GraphEdge edge in Edges) { - if (MathUtils.LinesIntersect(point, Center, edge.Point1 + Translation, edge.Point2 + Translation)) return false; + if (MathUtils.LinesIntersect(point, Center, edge.Point1 + Translation, edge.Point2 + Translation)) { return false; } } - return true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs index 1be30c7bb..9be5973f4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs @@ -135,12 +135,6 @@ namespace Barotrauma sw.Stop(); GameMain.PerformanceCounter.AddElapsedTicks("ParticleUpdate", sw.ElapsedTicks); sw.Restart(); - - GameMain.LightManager.Update((float)deltaTime); - - sw.Stop(); - GameMain.PerformanceCounter.AddElapsedTicks("LightUpdate", sw.ElapsedTicks); - sw.Restart(); if (Level.Loaded != null) Level.Loaded.Update((float)deltaTime, cam); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index 37b24658a..38d644e26 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs @@ -20,6 +20,11 @@ namespace Barotrauma public float MinValueFloat = float.MinValue, MaxValueFloat = float.MaxValue; public float ValueStep; + /// + /// Labels of the components of a vector property (defaults to x,y,z,w) + /// + public string[] VectorComponentLabels; + /// /// Currently implemented only for int fields. TODO: implement the remaining types (SerializableEntityEditor) /// @@ -57,6 +62,11 @@ namespace Barotrauma public bool isSaveable; public string translationTextTag; + /// + /// If set to true, the instance values saved in a submarine file will always override the prefab values, even if using a mod that normally overrides instance values. + /// + public bool AlwaysUseInstanceValues; + public string Description; /// @@ -65,13 +75,15 @@ namespace Barotrauma /// The property is set to this value during deserialization if the value is not defined in XML. /// Is the value saved to XML when serializing. /// If set to anything else than null, SerializableEntityEditors will show what the text gets translated to or warn if the text is not found in the language files. + /// If set to true, the instance values saved in a submarine file will always override the prefab values, even if using a mod that normally overrides instance values. /// Setting the value to a non-empty string will let the user select the text from one whose tag starts with the given string (e.g. RoomName. would show all texts with a RoomName.* tag) - public Serialize(object defaultValue, bool isSaveable, string description = "", string translationTextTag = null) + public Serialize(object defaultValue, bool isSaveable, string description = "", string translationTextTag = null, bool alwaysUseInstanceValues = false) { this.defaultValue = defaultValue; this.isSaveable = isSaveable; this.translationTextTag = translationTextTag; - this.Description = description; + Description = description; + AlwaysUseInstanceValues = alwaysUseInstanceValues; } } @@ -98,6 +110,8 @@ namespace Barotrauma public readonly AttributeCollection Attributes; public readonly Type PropertyType; + public readonly bool OverridePrefabValues; + public PropertyInfo PropertyInfo { get; private set; } public SerializableProperty(PropertyDescriptor property) @@ -107,6 +121,7 @@ namespace Barotrauma PropertyInfo = property.ComponentType.GetProperty(property.Name); PropertyType = property.PropertyType; Attributes = property.Attributes; + OverridePrefabValues = GetAttribute()?.AlwaysUseInstanceValues ?? false; } public T GetAttribute() where T : Attribute @@ -116,7 +131,7 @@ namespace Barotrauma if (a is T) return (T)a; } - return default(T); + return default; } public void SetValue(object parentObject, object val) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index 3c3f636d2..4ed031b1f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -1,5 +1,5 @@ using System; -using System.IO; +using Barotrauma.IO; using System.Collections.Generic; using System.Globalization; using System.Linq; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs b/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs index dbaceaa53..4b93e0dc1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Sprite/ConditionalSprite.cs @@ -1,21 +1,38 @@ using System.Collections.Generic; using System.Xml.Linq; using System.Linq; +using System; namespace Barotrauma { partial class ConditionalSprite { public readonly List conditionals = new List(); - public bool IsActive => Target != null && conditionals.All(c => c.Matches(Target)); + public bool IsActive + { + get + { + if (Target == null) { return false; } + return Comparison == PropertyConditional.Comparison.And ? conditionals.All(c => c.Matches(Target)) : conditionals.Any(c => c.Matches(Target)); + } + } + + public readonly PropertyConditional.Comparison Comparison; + public readonly bool Exclusive; 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 = "", bool lazyLoad = false) + public ConditionalSprite(XElement element, ISerializableEntity target, string file = "", bool lazyLoad = false) { Target = target; + Exclusive = element.GetAttributeBool("exclusive", Exclusive); + string comparison = element.GetAttributeString("comparison", null); + if (comparison != null) + { + Enum.TryParse(comparison, ignoreCase: true, out Comparison); + } foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -30,10 +47,10 @@ namespace Barotrauma } break; case "sprite": - Sprite = new Sprite(subElement, path, file, lazyLoad: lazyLoad); + Sprite = new Sprite(subElement, file: file, lazyLoad: lazyLoad); break; case "deformablesprite": - DeformableSprite = new DeformableSprite(subElement, filePath: path, lazyLoad: lazyLoad); + DeformableSprite = new DeformableSprite(subElement, filePath: file, lazyLoad: lazyLoad); break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Sprite/DeformableSprite.cs b/Barotrauma/BarotraumaShared/SharedSource/Sprite/DeformableSprite.cs index 81ed6f9e8..7fcc37437 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Sprite/DeformableSprite.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Sprite/DeformableSprite.cs @@ -18,12 +18,12 @@ namespace Barotrauma public Sprite Sprite { get; private set; } - public DeformableSprite(XElement element, int? subdivisionsX = null, int? subdivisionsY = null, string filePath = "", bool lazyLoad = false) + public DeformableSprite(XElement element, int? subdivisionsX = null, int? subdivisionsY = null, string filePath = "", bool lazyLoad = false, bool invert = false) { Sprite = new Sprite(element, file: filePath, lazyLoad: lazyLoad); - InitProjSpecific(element, subdivisionsX, subdivisionsY, lazyLoad); + InitProjSpecific(element, subdivisionsX, subdivisionsY, lazyLoad, invert); } - partial void InitProjSpecific(XElement element, int? subdivisionsX, int? subdivisionsY, bool lazyLoad = false); + partial void InitProjSpecific(XElement element, int? subdivisionsX, int? subdivisionsY, bool lazyLoad, bool invert); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs index e494349c4..fa7e514ca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Xml.Linq; using System.Linq; using Barotrauma.Extensions; -using System.IO; +using Barotrauma.IO; using System; using SpriteParams = Barotrauma.RagdollParams.SpriteParams; #if CLIENT @@ -47,7 +47,11 @@ namespace Barotrauma //the offset used when drawing the sprite protected Vector2 offset; - private bool lazyLoad; + public bool LazyLoad + { + get; + private set; + } protected Vector2 origin; @@ -108,6 +112,8 @@ namespace Barotrauma public string FullPath { get; private set; } + public bool Compress { get; private set; } + public override string ToString() { return FilePath + ": " + sourceRect; @@ -135,7 +141,7 @@ namespace Barotrauma public Sprite(XElement element, string path = "", string file = "", bool lazyLoad = false) { if (element == null) { return; } - this.lazyLoad = lazyLoad; + this.LazyLoad = lazyLoad; SourceElement = element; if (!ParseTexturePath(path, file)) { return; } Name = SourceElement.GetAttributeString("name", null); @@ -145,6 +151,7 @@ namespace Barotrauma { sourceVector = overrideElement.GetAttributeVector4("sourcerect", Vector4.Zero); } + Compress = SourceElement.GetAttributeBool("compress", true); bool shouldReturn = false; if (!lazyLoad) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs index 7eb494f1f..d491eea29 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs @@ -242,7 +242,7 @@ namespace Barotrauma case ConditionType.SpeciesName: if (target == null) { return Operator == OperatorType.NotEquals; } if (!(target is Character targetCharacter)) { return false; } - return (Operator == OperatorType.Equals) == (targetCharacter.SpeciesName == valStr); + return (Operator == OperatorType.Equals) == targetCharacter.SpeciesName.Equals(valStr, StringComparison.OrdinalIgnoreCase); case ConditionType.EntityType: switch (valStr) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 82b3b0c3d..da6a3d0e8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -104,7 +104,7 @@ namespace Barotrauma } } - class CharacterSpawnInfo : ISerializableEntity + public class CharacterSpawnInfo : ISerializableEntity { public string Name => $"Character Spawn Info ({SpeciesName})"; public Dictionary SerializableProperties { get; set; } @@ -188,6 +188,11 @@ namespace Barotrauma private set; } + public IEnumerable SpawnCharacters + { + get { return spawnCharacters; } + } + private readonly List> reduceAffliction; //only applicable if targeting NearbyCharacters or NearbyItems @@ -197,6 +202,8 @@ namespace Barotrauma private set; } + public Vector2 Offset { get; private set; } + public string Tags { get { return string.Join(",", tags); } @@ -234,6 +241,7 @@ namespace Barotrauma tags = new HashSet(element.GetAttributeString("tags", "").Split(',')); Range = element.GetAttributeFloat("range", 0.0f); + Offset = element.GetAttributeVector2("offset", Vector2.Zero); string[] targetLimbNames = element.GetAttributeStringArray("targetlimb", null) ?? element.GetAttributeStringArray("targetlimbs", null); if (targetLimbNames != null) { @@ -514,34 +522,56 @@ namespace Barotrauma switch (conditionalComparison) { case PropertyConditional.Comparison.Or: - foreach (ISerializableEntity target in targets) + foreach (PropertyConditional pc in propertyConditionals) { - foreach (PropertyConditional pc in propertyConditionals) + if (pc.TargetContainer && !targetingContainer) { - if (!string.IsNullOrEmpty(pc.TargetItemComponentName)) + var target = targets.FirstOrDefault(t => t is Item || t is ItemComponent); + var targetItem = target as Item ?? (target as ItemComponent)?.Item; + if (targetItem?.ParentInventory == null) { continue; } + if (targetItem.ParentInventory.Owner is Item container && HasRequiredConditions(container.AllPropertyObjects, targetingContainer: true)) { return true; } + if (targetItem.ParentInventory.Owner is Character character && HasRequiredConditions(character.ToEnumerable(), targetingContainer: true)) { return true; } + } + else + { + foreach (ISerializableEntity target in targets) { - if (!(target is ItemComponent ic) || ic.Name != pc.TargetItemComponentName) + if (!string.IsNullOrEmpty(pc.TargetItemComponentName)) { - continue; + if (!(target is ItemComponent ic) || ic.Name != pc.TargetItemComponentName) + { + continue; + } } + if (pc.Matches(target)) { return true; } } - if (pc.Matches(target)) { return true; } } } return false; case PropertyConditional.Comparison.And: - foreach (ISerializableEntity target in targets) + foreach (PropertyConditional pc in propertyConditionals) { - foreach (PropertyConditional pc in propertyConditionals) + if (pc.TargetContainer && !targetingContainer) { - if (!string.IsNullOrEmpty(pc.TargetItemComponentName)) + var target = targets.FirstOrDefault(t => t is Item || t is ItemComponent); + var targetItem = target as Item ?? (target as ItemComponent)?.Item; + if (targetItem?.ParentInventory == null) { return false; } + if (targetItem.ParentInventory.Owner is Item container && !HasRequiredConditions(container.AllPropertyObjects, targetingContainer: true)) { return false; } + if (targetItem.ParentInventory.Owner is Character character && !HasRequiredConditions(character.ToEnumerable(), targetingContainer: true)) { return false; } + } + else + { + foreach (ISerializableEntity target in targets) { - if (!(target is ItemComponent ic) || ic.Name != pc.TargetItemComponentName) + if (!string.IsNullOrEmpty(pc.TargetItemComponentName)) { - continue; + if (!(target is ItemComponent ic) || ic.Name != pc.TargetItemComponentName) + { + continue; + } } + if (!pc.Matches(target)) { return false; } } - if (!pc.Matches(target)) { return false; } } } return true; @@ -599,8 +629,11 @@ namespace Barotrauma { //ignore if not stackable and there's already an identical statuseffect DurationListElement existingEffect = DurationList.Find(d => d.Parent == this && d.Targets.FirstOrDefault() == target); - existingEffect?.Reset(Math.Max(existingEffect.Timer, duration), user); - return; + if (existingEffect != null) + { + existingEffect.Reset(Math.Max(existingEffect.Timer, duration), user); + return; + } } if (!HasRequiredConditions(target.ToEnumerable())) { return; } @@ -659,8 +692,8 @@ namespace Barotrauma hull = ((Item)entity).CurrentHull; } - Vector2 position = worldPosition ?? entity.WorldPosition; - if (targetLimbs?.FirstOrDefault(l => l != LimbType.None) is LimbType l) + Vector2 position = worldPosition ?? (entity.Removed ? Vector2.Zero : entity.WorldPosition); + if (worldPosition == null && targetLimbs?.FirstOrDefault(l => l != LimbType.None) is LimbType l) { if (entity is Character c) { @@ -671,6 +704,7 @@ namespace Barotrauma } } } + position += Offset; foreach (ISerializableEntity serializableEntity in targets) { @@ -716,13 +750,20 @@ namespace Barotrauma { if (target is Entity targetEntity) { - if (targetEntity.Removed) continue; + if (targetEntity.Removed) { continue; } + } + + if (target is Limb limb) + { + position = limb.WorldPosition + Offset; } for (int i = 0; i < propertyNames.Length; i++) { - if (target == null || target.SerializableProperties == null || - !target.SerializableProperties.TryGetValue(propertyNames[i], out SerializableProperty property)) continue; + if (target == null || target.SerializableProperties == null || !target.SerializableProperties.TryGetValue(propertyNames[i], out SerializableProperty property)) + { + continue; + } ApplyToProperty(target, property, propertyEffects[i], deltaTime); } } @@ -738,7 +779,10 @@ namespace Barotrauma foreach (Affliction affliction in Afflictions) { Affliction multipliedAffliction = affliction; - if (!disableDeltaTime) multipliedAffliction = affliction.CreateMultiplied(deltaTime); + if (!disableDeltaTime) + { + multipliedAffliction = affliction.CreateMultiplied(deltaTime); + } if (target is Character character) { @@ -746,18 +790,21 @@ namespace Barotrauma character.LastDamageSource = entity; foreach (Limb limb in character.AnimController.Limbs) { + if (limb.Removed) { continue; } + if (limb.IsSevered) { continue; } if (targetLimbs != null && !targetLimbs.Contains(limb.type)) { continue; } - limb.character.DamageLimb(position, limb, multipliedAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source); - limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability); + AttackResult result = limb.character.DamageLimb(position, limb, multipliedAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source); + limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, result.Damage); //only apply non-limb-specific afflictions to the first limb if (!affliction.Prefab.LimbSpecific) { break; } } } else if (target is Limb limb) { + if (limb.IsSevered) { continue; } if (limb.character.Removed || limb.Removed) { continue; } - limb.character.DamageLimb(position, limb, multipliedAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source); - limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability); + AttackResult result = limb.character.DamageLimb(position, limb, multipliedAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source); + limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, result.Damage); } } @@ -779,8 +826,15 @@ namespace Barotrauma { float prevVitality = targetCharacter.Vitality; targetCharacter.CharacterHealth.ReduceAffliction(targetLimb, reduceAffliction.First, reduceAmount); + if (user != null && user != targetCharacter) + { + if (!targetCharacter.IsDead) + { + targetCharacter.TryAdjustAttackerSkill(user, targetCharacter.Vitality - prevVitality); + } + }; #if SERVER - GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, user, prevVitality - targetCharacter.Vitality); + GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, user, prevVitality - targetCharacter.Vitality, 0.0f); #endif } } @@ -872,21 +926,24 @@ namespace Barotrauma private void ApplyToProperty(ISerializableEntity target, SerializableProperty property, object value, float deltaTime) { - if (disableDeltaTime || setValue) deltaTime = 1.0f; - + if (disableDeltaTime || setValue) { deltaTime = 1.0f; } Type type = value.GetType(); - if (type == typeof(float) || - (type == typeof(int) && property.GetValue(target) is float)) + if (type == typeof(float) || (type == typeof(int) && property.GetValue(target) is float)) { float floatValue = Convert.ToSingle(value) * deltaTime; - - if (!setValue) floatValue += (float)property.GetValue(target); + if (!setValue) + { + floatValue += (float)property.GetValue(target); + } property.TrySetValue(target, floatValue); } else if (type == typeof(int) && value is int) { int intValue = (int)((int)value * deltaTime); - if (!setValue) intValue += (int)property.GetValue(target); + if (!setValue) + { + intValue += (int)property.GetValue(target); + } property.TrySetValue(target, intValue); } else if (type == typeof(bool) && value is bool) @@ -975,8 +1032,15 @@ namespace Barotrauma { float prevVitality = targetCharacter.Vitality; targetCharacter.CharacterHealth.ReduceAffliction(targetLimb, reduceAffliction.First, reduceAffliction.Second * deltaTime); + if (element.User != null && element.User != targetCharacter) + { + if (!targetCharacter.IsDead) + { + targetCharacter.TryAdjustAttackerSkill(element.User, targetCharacter.Vitality - prevVitality); + } + }; #if SERVER - GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, element.User, prevVitality - targetCharacter.Vitality); + GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, element.User, prevVitality - targetCharacter.Vitality, 0.0f); #endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs index 66b7a1473..0fc57d9d5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs @@ -67,16 +67,19 @@ namespace Barotrauma foreach (Character c in Character.CharacterList) { - if (c.IsDead) continue; + if (c.IsDead) { continue; } //achievement for descending below crush depth and coming back - if (c.WorldPosition.Y < SubmarineBody.DamageDepth || (c.Submarine != null && c.Submarine.WorldPosition.Y < SubmarineBody.DamageDepth)) + if (Timing.TotalTime > GameMain.GameSession.RoundStartTime + 30.0f) { - roundData.EnteredCrushDepth.Add(c); - } - else if (c.WorldPosition.Y > SubmarineBody.DamageDepth * 0.5f) - { - //all characters that have entered crush depth and are still alive get an achievement - if (roundData.EnteredCrushDepth.Contains(c)) UnlockAchievement(c, "survivecrushdepth"); + if (c.WorldPosition.Y < SubmarineBody.DamageDepth || (c.Submarine != null && c.Submarine.WorldPosition.Y < SubmarineBody.DamageDepth)) + { + roundData.EnteredCrushDepth.Add(c); + } + else if (c.WorldPosition.Y > SubmarineBody.DamageDepth * 0.5f) + { + //all characters that have entered crush depth and are still alive get an achievement + if (roundData.EnteredCrushDepth.Contains(c)) UnlockAchievement(c, "survivecrushdepth"); + } } } @@ -106,7 +109,7 @@ namespace Barotrauma //achievement for descending ridiculously deep float realWorldDepth = Math.Abs(sub.Position.Y - Level.Loaded.Size.Y) * Physics.DisplayToRealWorldRatio; - if (realWorldDepth > 5000.0f) + if (realWorldDepth > 5000.0f && Timing.TotalTime > GameMain.GameSession.RoundStartTime + 30.0f) { //all conscious characters inside the sub get an achievement UnlockAchievement("subdeep", true, c => c != null && c.Submarine == sub && !c.IsDead && !c.IsUnconscious); @@ -122,7 +125,10 @@ namespace Barotrauma if (GameMain.GameSession != null) { #if CLIENT - if (Character.Controlled != null) { CheckMidRoundAchievements(Character.Controlled); } + if (Character.Controlled != null && !(GameMain.GameSession.GameMode is SubTestMode)) + { + CheckMidRoundAchievements(Character.Controlled); + } #else foreach (Client client in GameMain.Server.ConnectedClients) { @@ -288,7 +294,7 @@ namespace Barotrauma public static void OnRoundEnded(GameSession gameSession) { //made it to the destination - if (gameSession.Submarine.AtEndPosition && Level.Loaded != null) + if (gameSession?.Submarine != null && Level.Loaded != null && gameSession.Submarine.AtEndPosition) { float levelLengthMeters = Physics.DisplayToRealWorldRatio * Level.Loaded.Size.X; float levelLengthKilometers = levelLengthMeters / 1000.0f; @@ -368,10 +374,16 @@ namespace Barotrauma if (charactersInSub.Count == 1) { //there must be some non-enemy casualties to get the last mant standing achievement - if (roundData.Casualties.Any(c => !(c.AIController is EnemyAIController))) + if (roundData.Casualties.Any(c => !(c.AIController is EnemyAIController) && c.TeamID == charactersInSub[0].TeamID)) { UnlockAchievement(charactersInSub[0], "lastmanstanding"); } +#if CLIENT + else if (GameMain.GameSession.CrewManager.GetCharacters().Count() == 1) + { + UnlockAchievement(charactersInSub[0], "lonesailor"); + } +#else //lone sailor achievement if alone in the sub and there are no other characters with the same team ID else if (!Character.CharacterList.Any(c => c != charactersInSub[0] && @@ -380,6 +392,8 @@ namespace Barotrauma { UnlockAchievement(charactersInSub[0], "lonesailor"); } +#endif + } foreach (Character character in charactersInSub) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs b/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs index 8f57a7858..d201dc125 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; @@ -304,6 +304,7 @@ namespace Barotrauma { for (int i = 0; i < variableTags.Length; i++) { + if (string.IsNullOrEmpty(variableValues[i])) { continue; } if (formatCapitals[i]) { variableValues[i] = HandleVariableCapitalization(text, variableTags[i], variableValues[i]); @@ -313,6 +314,13 @@ namespace Barotrauma for (int i = 0; i < variableTags.Length; i++) { + if (variableValues[i] == null) + { +#if DEBUG + DebugConsole.ThrowError("Error in TextManager.GetWithVariables (variable " + i + " was null).\n" + Environment.StackTrace); +#endif + continue; + } text = text.Replace(variableTags[i], variableValues[i]); } @@ -643,6 +651,37 @@ namespace Barotrauma } } + /// + /// Fetches a single variable from a servermessage + /// + public static string GetServerMessageVariable(string message, string variable) + { + int variableIndex = message.IndexOf(variable); + if (variableIndex == -1) + { +#if DEBUG + DebugConsole.ThrowError($"Server message variable: '{variable}' not found in message: '{message}'"); +#endif + return string.Empty; + } + + int startIndex = message.IndexOf('=', variableIndex) + 1; + int endIndex = startIndex; + + for (int i = startIndex; i < message.Length; i++) + { + if (message[i] == '/' || message[i] == '~') + { + endIndex = i; + break; + } + } + + if (endIndex == startIndex) endIndex = message.Length; + + return message.Substring(startIndex, endIndex - startIndex); + } + public static bool IsServerMessageWithVariables(string message) { for (int i = 0; i < serverMessageCharacters.Length; i++) diff --git a/Barotrauma/BarotraumaShared/SharedSource/TextPack.cs b/Barotrauma/BarotraumaShared/SharedSource/TextPack.cs index 26ad16718..06526e2ab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/TextPack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/TextPack.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text; using System.Threading; using System.Xml.Linq; +using Barotrauma.IO; namespace Barotrauma { @@ -34,7 +35,7 @@ namespace Barotrauma { //try fixing legacy EnglishVanilla path string newPath = "Content/Texts/English/EnglishVanilla.xml"; - if (System.IO.File.Exists(newPath)) + if (Barotrauma.IO.File.Exists(newPath)) { DebugConsole.NewMessage("Content package is using the obsolete text file path \"" + filePath + "\". Attempting to load from \"" + newPath + "\"..."); this.FilePath = filePath = newPath; @@ -172,9 +173,7 @@ namespace Barotrauma } } - System.IO.StreamWriter file = new System.IO.StreamWriter(@"duplicate_" + Language.ToLower() + "_" + index + ".txt"); - file.WriteLine(sb.ToString()); - file.Close(); + File.WriteAllText(@"duplicate_" + Language.ToLower() + "_" + index + ".txt", sb.ToString()); } public void WriteToCSV(int index) @@ -199,9 +198,7 @@ namespace Barotrauma } } - System.IO.StreamWriter file = new System.IO.StreamWriter(@"csv_" + Language.ToLower() + "_" + index + ".csv"); - file.WriteLine(sb.ToString()); - file.Close(); + File.WriteAllText(@"csv_" + Language.ToLower() + "_" + index + ".csv", sb.ToString()); } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/RichTextData.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/RichTextData.cs new file mode 100644 index 000000000..eb4fac829 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/RichTextData.cs @@ -0,0 +1,85 @@ +using Microsoft.Xna.Framework; +using System.Collections.Generic; + +namespace Barotrauma +{ + public class RichTextData + { + public int StartIndex, EndIndex; + public Color? Color; + public string Metadata; + + private const char definitionIndicator = '‖'; + private const char attributeSeparator = ';'; + private const char keyValueSeparator = ':'; + //private const char lineChangeIndicator = '\n'; + + private const string colorDefinition = "color"; + private const string metadataDefinition = "metadata"; + private const string endDefinition = "end"; + + public static List GetRichTextData(string text, out string sanitizedText) + { + List textColors = null; + sanitizedText = text; + if (!string.IsNullOrEmpty(text) && text.Contains(definitionIndicator)) + { + string[] segments = text.Split(definitionIndicator); + + sanitizedText = string.Empty; + + textColors = new List(); + RichTextData tempData = null; + + int prevIndex = 0; + int currIndex = 0; + for (int i=0;i GetInvalidFileNameChars() + { + return System.IO.Path.GetInvalidFileNameChars(); + } + + } + + public static class Directory + { + public static string GetCurrentDirectory() + { + return System.IO.Directory.GetCurrentDirectory(); + } + + public static void SetCurrentDirectory(string path) + { + System.IO.Directory.SetCurrentDirectory(path); + } + + public static IEnumerable GetFiles(string path) + { + return System.IO.Directory.GetFiles(path); + } + + public static IEnumerable GetFiles(string path, string pattern, System.IO.SearchOption option = System.IO.SearchOption.AllDirectories) + { + return System.IO.Directory.GetFiles(path, pattern, option); + } + + public static IEnumerable GetDirectories(string path) + { + return System.IO.Directory.GetDirectories(path); + } + + public static IEnumerable GetFileSystemEntries(string path) + { + return System.IO.Directory.GetFileSystemEntries(path); + } + + public static IEnumerable EnumerateDirectories(string path, string pattern) + { + return System.IO.Directory.EnumerateDirectories(path, pattern); + } + + public static IEnumerable EnumerateFiles(string path, string pattern) + { + return System.IO.Directory.EnumerateFiles(path, pattern); + } + + public static bool Exists(string path) + { + return System.IO.Directory.Exists(path); + } + + public static System.IO.DirectoryInfo CreateDirectory(string path) + { + if (!Validation.CanWrite(path)) + { + DebugConsole.ThrowError($"Cannot create directory \"{path}\": failed validation"); + return null; + } + return System.IO.Directory.CreateDirectory(path); + } + + public static void Delete(string path, bool recursive=true) + { + if (!Validation.CanWrite(path)) + { + DebugConsole.ThrowError($"Cannot delete directory \"{path}\": failed validation"); + return; + } + //TODO: validate recursion? + System.IO.Directory.Delete(path, recursive); + } + } + + public static class File + { + public static bool Exists(string path) + { + return System.IO.File.Exists(path); + } + + public static void Copy(string src, string dest, bool overwrite=false) + { + if (!Validation.CanWrite(dest)) + { + DebugConsole.ThrowError($"Cannot copy \"{src}\" to \"{dest}\": failed validation"); + return; + } + System.IO.File.Copy(src, dest, overwrite); + } + + public static void Move(string src, string dest) + { + if (!Validation.CanWrite(src)) + { + DebugConsole.ThrowError($"Cannot move \"{src}\" to \"{dest}\": src failed validation"); + return; + } + if (!Validation.CanWrite(dest)) + { + DebugConsole.ThrowError($"Cannot move \"{src}\" to \"{dest}\": dest failed validation"); + return; + } + System.IO.File.Move(src, dest); + } + + public static void Delete(string path) + { + if (!Validation.CanWrite(path)) + { + DebugConsole.ThrowError($"Cannot delete file \"{path}\": failed validation"); + return; + } + System.IO.File.Delete(path); + } + + public static DateTime GetLastWriteTime(string path) + { + return System.IO.File.GetLastWriteTime(path); + } + + public static FileStream Open(string path, System.IO.FileMode mode, System.IO.FileAccess access = System.IO.FileAccess.ReadWrite) + { + switch (mode) + { + case System.IO.FileMode.Create: + case System.IO.FileMode.CreateNew: + case System.IO.FileMode.OpenOrCreate: + case System.IO.FileMode.Append: + case System.IO.FileMode.Truncate: + if (!Validation.CanWrite(path)) + { + DebugConsole.ThrowError($"Cannot open \"{path}\" in {mode} mode: failed validation"); + return null; + } + break; + } + return new FileStream(path, System.IO.File.Open(path, mode, + !Validation.CanWrite(path, false) ? + System.IO.FileAccess.Read : + access)); + } + + public static FileStream OpenRead(string path) + { + return Open(path, System.IO.FileMode.Open, System.IO.FileAccess.Read); + } + + public static FileStream OpenWrite(string path) + { + return Open(path, System.IO.FileMode.OpenOrCreate, System.IO.FileAccess.Write); + } + + public static FileStream Create(string path) + { + return Open(path, System.IO.FileMode.Create, System.IO.FileAccess.Write); + } + + public static void WriteAllBytes(string path, byte[] contents) + { + if (!Validation.CanWrite(path)) + { + DebugConsole.ThrowError($"Cannot write all bytes to \"{path}\": failed validation"); + return; + } + System.IO.File.WriteAllBytes(path, contents); + } + + public static void WriteAllText(string path, string contents, System.Text.Encoding? encoding = null) + { + if (!Validation.CanWrite(path)) + { + DebugConsole.ThrowError($"Cannot write all text to \"{path}\": failed validation"); + return; + } + System.IO.File.WriteAllText(path, contents, encoding ?? System.Text.Encoding.UTF8); + } + + public static void WriteAllLines(string path, IEnumerable contents, System.Text.Encoding? encoding = null) + { + if (!Validation.CanWrite(path)) + { + DebugConsole.ThrowError($"Cannot write all lines to \"{path}\": failed validation"); + return; + } + System.IO.File.WriteAllLines(path, contents, encoding ?? System.Text.Encoding.UTF8); + } + + public static byte[] ReadAllBytes(string path) + { + return System.IO.File.ReadAllBytes(path); + } + + public static string ReadAllText(string path, System.Text.Encoding? encoding = null) + { + return System.IO.File.ReadAllText(path, encoding ?? System.Text.Encoding.UTF8); + } + + public static string[] ReadAllLines(string path, System.Text.Encoding? encoding = null) + { + return System.IO.File.ReadAllLines(path, encoding ?? System.Text.Encoding.UTF8); + } + } + + public class FileStream : System.IO.Stream + { + private System.IO.FileStream innerStream; + private string fileName; + + public FileStream(string fn, System.IO.FileStream stream) + { + innerStream = stream; + fileName = fn; + } + + public override bool CanRead => innerStream.CanRead; + public override bool CanSeek => innerStream.CanSeek; + public override bool CanTimeout => innerStream.CanTimeout; + public override bool CanWrite + { + get + { + if (!Validation.CanWrite(fileName)) { return false; } + return innerStream.CanWrite; + } + } + + public override long Length => innerStream.Length; + + public override long Position + { + get + { + return innerStream.Position; + } + set + { + innerStream.Position = value; + } + } + + public override int Read(byte[] buffer, int offset, int count) + { + return innerStream.Read(buffer, offset, count); + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (Validation.CanWrite(fileName)) + { + innerStream.Write(buffer, offset, count); + } + else + { + DebugConsole.ThrowError($"Cannot write to file \"{fileName}\": failed validation"); + } + } + + public override long Seek(long offset, System.IO.SeekOrigin origin) + { + return innerStream.Seek(offset, origin); + } + + public override void SetLength(long value) + { + innerStream.SetLength(value); + } + + public override void Flush() + { + innerStream.Flush(); + } + + protected override void Dispose(bool disposing) + { + innerStream.Dispose(); + } + } + + public class DirectoryInfo + { + private System.IO.DirectoryInfo innerInfo; + + public DirectoryInfo(string path) + { + innerInfo = new System.IO.DirectoryInfo(path); + } + + private DirectoryInfo(System.IO.DirectoryInfo info) + { + innerInfo = info; + } + + public bool Exists => innerInfo.Exists; + public string Name => innerInfo.Name; + public string FullName => innerInfo.FullName; + + public System.IO.FileAttributes Attributes => innerInfo.Attributes; + + public IEnumerable GetDirectories() + { + var dirs = innerInfo.GetDirectories(); + foreach (var dir in dirs) + { + yield return new DirectoryInfo(dir); + } + } + + public IEnumerable GetFiles() + { + var files = innerInfo.GetFiles(); + foreach (var file in files) + { + yield return new FileInfo(file); + } + } + + public void Delete() + { + if (!Validation.CanWrite(innerInfo.FullName)) + { + DebugConsole.ThrowError($"Cannot delete directory \"{Name}\": failed validation"); + return; + } + innerInfo.Delete(); + } + } + + public class FileInfo + { + private System.IO.FileInfo innerInfo; + + public FileInfo(string path) + { + innerInfo = new System.IO.FileInfo(path); + } + + public FileInfo(System.IO.FileInfo info) + { + innerInfo = info; + } + + public bool Exists => innerInfo.Exists; + public string Name => innerInfo.Name; + public string FullName => innerInfo.FullName; + public long Length => innerInfo.Length; + + public bool IsReadOnly + { + get + { + return innerInfo.IsReadOnly; + } + set + { + if (!Validation.CanWrite(innerInfo.FullName)) + { + DebugConsole.ThrowError($"Cannot set read-only to {value} for \"{Name}\": failed validation"); + return; + } + innerInfo.IsReadOnly = value; + } + } + + public void CopyTo(string dest, bool overwriteExisting = false) + { + if (!Validation.CanWrite(dest)) + { + DebugConsole.ThrowError($"Cannot copy \"{Name}\" to \"{dest}\": failed validation"); + return; + } + innerInfo.CopyTo(dest, overwriteExisting); + } + + public void Delete() + { + if (!Validation.CanWrite(innerInfo.FullName)) + { + DebugConsole.ThrowError($"Cannot delete file \"{Name}\": failed validation"); + return; + } + innerInfo.Delete(); + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs index 30ab6888d..5aa2a9611 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.IO.Compression; using System.Linq; using System.Text; @@ -245,7 +245,7 @@ namespace Barotrauma // B. // Read file into byte array buffer. byte[] b; - using (FileStream f = new FileStream(temp, FileMode.Open)) + using (FileStream f = File.Open(temp, System.IO.FileMode.Open)) { b = new byte[f.Length]; f.Read(b, 0, (int)f.Length); @@ -253,7 +253,7 @@ namespace Barotrauma // C. // Use GZipStream to write compressed bytes to target file. - using (FileStream f2 = new FileStream(fileName, FileMode.Create)) + using (FileStream f2 = File.Open(fileName, System.IO.FileMode.Create)) using (GZipStream gz = new GZipStream(f2, CompressionMode.Compress, false)) { gz.Write(b, 0, b.Length); @@ -276,10 +276,10 @@ namespace Barotrauma public static void CompressDirectory(string sInDir, string sOutFile, ProgressDelegate progress) { - string[] sFiles = Directory.GetFiles(sInDir, "*.*", SearchOption.AllDirectories); + IEnumerable sFiles = Directory.GetFiles(sInDir, "*.*", System.IO.SearchOption.AllDirectories); int iDirLen = sInDir[sInDir.Length - 1] == Path.DirectorySeparatorChar ? sInDir.Length : sInDir.Length + 1; - using (FileStream outFile = new FileStream(sOutFile, FileMode.Create, FileAccess.Write, FileShare.None)) + using (FileStream outFile = File.Open(sOutFile, System.IO.FileMode.Create, System.IO.FileAccess.Write)) using (GZipStream str = new GZipStream(outFile, CompressionMode.Compress)) foreach (string sFilePath in sFiles) { @@ -290,11 +290,11 @@ namespace Barotrauma } - public static Stream DecompressFiletoStream(string fileName) + public static System.IO.Stream DecompressFiletoStream(string fileName) { - using (FileStream originalFileStream = new FileStream(fileName, FileMode.Open)) + using (FileStream originalFileStream = File.Open(fileName, System.IO.FileMode.Open)) { - MemoryStream decompressedFileStream = new MemoryStream(); + System.IO.MemoryStream decompressedFileStream = new System.IO.MemoryStream(); using (GZipStream decompressionStream = new GZipStream(originalFileStream, CompressionMode.Decompress)) { @@ -347,13 +347,13 @@ namespace Barotrauma { try { - using (FileStream outFile = new FileStream(sFilePath, FileMode.Create, FileAccess.Write, FileShare.None)) + using (FileStream outFile = File.Open(sFilePath, System.IO.FileMode.Create, System.IO.FileAccess.Write)) { outFile.Write(bytes, 0, iFileLen); } break; } - catch (IOException e) + catch (System.IO.IOException e) { if (i >= maxRetries || !File.Exists(sFilePath)) { throw; } DebugConsole.NewMessage("Failed decompress file \"" + sFilePath + "\" {" + e.Message + "}, retrying in 250 ms...", Color.Red); @@ -371,13 +371,13 @@ namespace Barotrauma { try { - using (FileStream inFile = new FileStream(sCompressedFile, FileMode.Open, FileAccess.Read, FileShare.None)) + using (FileStream inFile = File.Open(sCompressedFile, System.IO.FileMode.Open, System.IO.FileAccess.Read)) using (GZipStream zipStream = new GZipStream(inFile, CompressionMode.Decompress, true)) while (DecompressFile(sDir, zipStream, progress)) { }; break; } - catch (IOException e) + catch (System.IO.IOException e) { if (i >= maxRetries || !File.Exists(sCompressedFile)) { throw; } DebugConsole.NewMessage("Failed decompress file \"" + sCompressedFile + "\" {" + e.Message + "}, retrying in 250 ms...", Color.Red); @@ -393,12 +393,12 @@ namespace Barotrauma if (!dir.Exists) { - throw new DirectoryNotFoundException( + throw new System.IO.DirectoryNotFoundException( "Source directory does not exist or could not be found: " + sourceDirName); } - DirectoryInfo[] dirs = dir.GetDirectories(); + IEnumerable dirs = dir.GetDirectories(); // If the destination directory doesn't exist, create it. if (!Directory.Exists(destDirName)) { @@ -406,11 +406,12 @@ namespace Barotrauma } // Get the files in the directory and copy them to the new location. - FileInfo[] files = dir.GetFiles(); + IEnumerable files = dir.GetFiles(); foreach (FileInfo file in files) { string tempPath = Path.Combine(destDirName, file.Name); - file.CopyTo(tempPath, overwriteExisting); + if (!overwriteExisting && File.Exists(tempPath)) { continue; } + file.CopyTo(tempPath, true); } // If copying subdirectories, copy them and their contents to new location. @@ -472,7 +473,7 @@ namespace Barotrauma di.Delete(); break; } - catch (IOException) + catch (System.IO.IOException) { if (i >= maxRetries) { throw; } Thread.Sleep(250); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index e8ff1a8e9..21251f8de 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -4,7 +4,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Reflection; using System.Security.Cryptography; @@ -325,15 +325,12 @@ namespace Barotrauma { try { - using (StreamReader file = new StreamReader(filePath)) + lines = File.ReadAllLines(filePath).ToList(); + cachedLines.Add(filePath, lines); + if (lines.Count == 0) { - lines = File.ReadLines(filePath).ToList(); - cachedLines.Add(filePath, lines); - if (lines.Count == 0) - { - DebugConsole.ThrowError("File \"" + filePath + "\" is empty!"); - return ""; - } + DebugConsole.ThrowError("File \"" + filePath + "\" is empty!"); + return ""; } } catch (Exception e) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/UpdaterUtil.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/UpdaterUtil.cs index 721224dbb..dcd806d14 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/UpdaterUtil.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/UpdaterUtil.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.IO; +using Barotrauma.IO; using System.Linq; using System.Security.Cryptography; using System.Xml.Linq; @@ -15,7 +15,7 @@ namespace Barotrauma { XDocument doc = new XDocument(CreateFileList()); - doc.Save(filePath); + doc.SaveSafe(filePath); } public static XElement CreateFileList() @@ -23,7 +23,7 @@ namespace Barotrauma XElement root = new XElement("filelist"); string currentDir = Directory.GetCurrentDirectory(); - string[] files = Directory.GetFiles(currentDir, "*", SearchOption.AllDirectories); + IEnumerable files = Directory.GetFiles(currentDir, "*", System.IO.SearchOption.AllDirectories); foreach (string file in files) { @@ -122,7 +122,7 @@ namespace Barotrauma /// public static void InstallUpdatedFiles(string updateFileFolder) { - string[] files = Directory.GetFiles(updateFileFolder, "*", SearchOption.AllDirectories); + IEnumerable files = Directory.GetFiles(updateFileFolder, "*", System.IO.SearchOption.AllDirectories); string currentDir = Directory.GetCurrentDirectory(); @@ -166,7 +166,7 @@ namespace Barotrauma { string currentDir = Directory.GetCurrentDirectory(); - string[] files = Directory.GetFiles(currentDir, "*", SearchOption.AllDirectories); + IEnumerable files = Directory.GetFiles(currentDir, "*", System.IO.SearchOption.AllDirectories); foreach (string file in files) { @@ -199,7 +199,7 @@ namespace Barotrauma { string currentDir = Directory.GetCurrentDirectory(); - string[] files = Directory.GetFiles(currentDir, "*", SearchOption.AllDirectories); + IEnumerable files = Directory.GetFiles(currentDir, "*", System.IO.SearchOption.AllDirectories); foreach (string file in files) { diff --git a/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub b/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub index 43935b665..e994927d5 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub and b/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Berilia.sub b/Barotrauma/BarotraumaShared/Submarines/Berilia.sub index a0e39932e..f849543c7 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Berilia.sub and b/Barotrauma/BarotraumaShared/Submarines/Berilia.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Dugong.sub b/Barotrauma/BarotraumaShared/Submarines/Dugong.sub index 2e9861259..59dfa6312 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Dugong.sub and b/Barotrauma/BarotraumaShared/Submarines/Dugong.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub b/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub index 8b5737bfe..6589ee31a 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub and b/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Humpback.sub b/Barotrauma/BarotraumaShared/Submarines/Humpback.sub index 4fa4b3639..b5dffcd21 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 81e94e034..08e0a7748 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 7ffd2212b..13b1e5aec 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub and b/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Remora.sub b/Barotrauma/BarotraumaShared/Submarines/Remora.sub index d3e55e1f8..3597af988 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Remora.sub and b/Barotrauma/BarotraumaShared/Submarines/Remora.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Typhon.sub b/Barotrauma/BarotraumaShared/Submarines/Typhon.sub index 5ef283918..fb04cfa0a 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Typhon.sub and b/Barotrauma/BarotraumaShared/Submarines/Typhon.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub b/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub index f6f1ec06f..05587fc0d 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub and b/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub differ diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 54e6ada20..2d289832b 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,117 @@ +--------------------------------------------------------------------------------------------------------- +v0.9.1000.0 (Unstable) +--------------------------------------------------------------------------------------------------------- + +Additions and changes: +- Added 2 new moloch variants: Black Moloch and Baby Moloch. +- Added DXT5 texture compression to reduce memory consumption. Slightly increases loading times; if you're not short on memory, you may want to disable the compression from the game settings. +- Modded servers show up as purple in the server list. +- Added 4 new background music tracks. +- Added parameter autocompletion to the "spawnsub" command. +- All content types except UI styles are now hotswappable. +- Made smoke detector logic more accurate (no size restrictions on the size of the fire, the fire doesn't have to be inside the same hull). +- Added "Output" and "FalseOutput" properties to smoke detector. +- Option to define ambient light values for individual hulls in the sub editor. +- Characters float in place instead of sinking when staying still underwater. +- Improvements to water flow forces: flowing water can push characters around much more heavily now. +- Balanced item prices and fabrication/deconstruction recipes. +- Balanced medical items. +- UI layout improvements when using an ultrawide resolution. +- Added "set_channel" input to wifi components. +- Added "power_value_out" and "load_value_out" outputs to reactor. +- Added search/filter boxes to content package list in the settings menu and item lists in the Workshop menu. +- Added submarine filter to the server lobby. +- Detonators are only triggered by non-zero signals. +- The state of toggleable controllers (= switches) can be set in the sub editor. +- Made toolboxes purchaseable. +- Added a warning to keep the drone door closed in Remora. +- All Thalamus cells die when the Thalamus dies. +- Removed "hold fire" option from the "operate weapon" order, display turrets on minimap as icons instead of text. +- Added some logic to prevent the game from modifying/deleting any vanilla content in any situation. +- Readded legacy Carrier (doesn't spawn naturally but can be spawned with console commands). +- Modified Typhon 2's coilgun rotation limits a bit so it isn't possible to hit the walls when firing at enemies near the airlock. +- Hulls can be multiedited in the sub editor. +- Placed down wires can now be re-equipped in the sub editor by double clicking a loose end. +- Added charging docks to Remora. +- Adjusted how pixel sizes are converted to meters (which are used to display the submarine's dimensions and distances on the navigation terminal). Previously 100 pixels corresponded to 1 meter, now it's 80px -> 1m, making the human characters about 1.75m tall. +- Distance calculations on the navigation terminal take the shape of the path into account instead of just using the direct distance to the target. +- Made improvements to the manual order assignment by adding always visible name labels, displaying indicators for characters' current orders, and repositioning the nodes. +- Reduced the damage range of fires, characters don't take damage from fires if there's a closed door or a wall in between. + +Modding: +- Made it possible to use repair tools with StatusEffect's UseItem. +- Made pressure deaths more moddable. Dying because of high pressure isn't hard-coded anymore, the characters are just given the barotrauma affliction which (by default) kills them. +- Added "HideConditionBar" property to items. +- Fixed wearables staying on the character when the item is removed by a status effect. + +Bugfixes: +- Fixed crashing when opening the tab menu when there are clients present with no job preferences set. +- Fixed "ColdCavernsMaze" levels sometimes being extremely short. +- Fixes to level generation when playing with a very large submarine. +- Fixed bots being unable to shoot at enemies from another room/hull. +- Fixed bots being unable to get items from dead bodies. +- Fixed submarines being unable to move vertically in the submarine test mode. +- Fixed crashing when starting a new round with no audio device (speakers, headset) available. +- Improvements to shadow/LOS rendering. +- Fixed double click being ignored if it's been less than 0.4s since the last double click. +- Fixed all servers sometimes not showing up in the server list (showing only the servers in the same/nearby region). +- Fixed "lone sailor" achievement not unlocking in single player. +- Fixed "Gaze in to the Abyss" achievement sometimes unlocking as soon as the round starts. +- Fixed characters sometimes being unable to exit the submarine when outside the borders of the level. +- Fixed ruin/wreck monsters not spawning if the submarine is too close to them. +- Fixed ruin items sometimes getting saved as a part of the main submarine in the campaign. +- Fixed rare "item with the same key has already been added" errors when starting a round (particularly when playing with a submarine with very large numbers of items/structures). +- Thalamus entities can't be selected in the sub editor when they're hidden. +- Fixed "spawnsub" console command not working. +- Fixed welding tools and plasma cutters hitting destroyed Thalamus organs. +- Fixed reactor not shutting down if the turbine/fission rate are controlled via signals even the power switch is toggled off. +- Fixed reactor sliders not moving when they're controlled by signals. +- Fixed level triggers sometimes affecting entities that have left the trigger. The most noticeable effect was characters getting burn damage indefinitely after they've been close to a hydrothermal vent. +- Made flamers fire proof to prevents the flames from blowing up the fuel tank inside it. +- Don't allow harpoons to stick to very small limbs (such as mudraptor's "mouth tentacles"), because it lead to physics glitches. +- Fixed the husk infection crashing when turning the character, if either the non-husk or the husked variation has no inventory defined. +- Fixed characters getting weapon XP after using a turret, until someone else operates the same turret. +- Fixed characters being able to crouch when their pose is controlled by a controller (e.g. periscope, modded chair). +- Fixed bots being unable to aim correctly when operating a turret in another submarine (such as a remotely controlled coilgun in a shuttle). +- Fixed bots sometimes taking too much time to interrupt their current objective when the room they're in floods, causing them to get crushed by pressure. +- Fixed bots sometimes letting go of ladders too soon, preventing them from reaching certain areas (such as the upper platform in Berilia's cargo bay). +- Fixed multiple bots sometimes trying to treat the same person. +- Bots stop grabbing the character they're treating after they're done. +- Fixed harpoons going through doors. +- Fixed depth charges going through level walls. +- Fixed husks attacking human husks wearing a diving suit. +- Fixed first shot from a firearm that uses a magazine/clip not doing anything. +- Fixes to waypoints in Kastrull and Remora. +- Fixed chat-linked wifi components not working in single player. +- Fixed chat-linked wifi components not working in multiplayer outside of combat missions. +- Fixed Azimuth using tutorial junction boxes instead of normal ones (the tutorial variants are indestructible and don't have signal connections). +- Fixed autopilot not being able to navigate past wrecks. +- More reliable syncing of door's breaking state. Fixes doors sometimes being impassable or impossible to repair client-side, particularly when joining mid-round. +- Fixed repairing doors getting interrupted at 50% when the door's collider is re-enabled and the character pushed out of the doorway. +- Fixed items getting used in the health interface when dropped on a subinventory slot in front of the interface. +- Fixed dedicated servers letting clients join with an invalid name when there's no server owner. +- Fixed server letting clients join with a name that's already taken. +- Changed "creating hulls between docking ports failed" from errors to warnings. Allows creating exterior docking ports that aren't next to a hull without having the console pop up every time the port is used. +- Fixed changing resolution not updating character inventory slot sizes properly, causing invalid spacing in dead characters' inventories. +- Don't allow detaching signal components when rewiring is disabled or the item's connection panel is locked. +- Fixed misaligned hull next to Wreck1's airlock. +- Fixed "attempting to remove an already removed item" errors when mass-deleting items. Happened because removing items a wire is connected to removes the orphaned wires automatically. +- Fixed multi-part subs (example case: The Aeche III) getting teleported to oblivion when flipped. +- Fixes to bots getting stuck or killed for no apparent reason when the player is very far from them, due to the bots switching to a "simple physics mode" which prevents them from doing certain kinds of interactions. + +--------------------------------------------------------------------------------------------------------- +v0.9.9.1 +--------------------------------------------------------------------------------------------------------- + +- Fixed bots being unable to fire turrets due to the visibility raycast hitting the turret's collider. +- Fixed submarine's version number resetting to 0.0.0.0 when saving in the sub editor (didn't affect the actual saved sub file, only the in-game metadata that got fixed by restarting the game - so there's no need to do anything to fix the sub files you saved with the previous version). +- Fixed server list occasionally crashing when trying to filter based on game mode. +- Fixed an issue in multiplayer campaign that occasionally caused clients to get kicked with a "missing entity" error message. +- Fixed clients occasionally crashing when joining a server mid-round. Happened when the client tried to execute the attack of a monster that has already despawned server-side. +- Fixed local changes an user has made to a mod getting overwritten when the item gets autoupdated. +- Changed the hotkey to toggle the entity list in the sub editor from Q to the "toggle inventory" keybind to make it a little more user-friendly on AZERTY keyboards. +- Fixed corpses in wrecks being considered dead members of the crew in SteamAchievementManager, preventing "lone sailor" from unlocking and making it possible to unlock "last man standing" with just one character. + --------------------------------------------------------------------------------------------------------- v0.9.9.0 --------------------------------------------------------------------------------------------------------- @@ -5,8 +119,14 @@ v0.9.9.0 - Added wrecked submarines to levels. - Reimplemented carrier (now called Thalamus). - New submarine, Azimuth. -- Option to disable bot conversations in multiplayer. +- Miscellaneous performance optimizations. - Improvements to traitor missions (slightly simpler, with clearer instructions). +- Reduced the skill requirements for mechanical repairs. +- Reduced the damage when a mechanical repair fails. +- Rebalanced mission rewards. +- New water ambience sounds. +- Speed up despawning when there are lots of bodies inside the sub, enemies despawn x2 faster. +- Salvage missions can be completed by taking the artifact to the start outpost (the descriptions don't specify which outpost to return it to). - Characters with insufficient skills can fail at mechanical repairs, causing minor injuries. - Gaps that are inside a hull don't flood the sub, a warning icon is displayed on those gaps in the sub editor. - Added submarine test mode to the sub editor. @@ -16,6 +136,64 @@ v0.9.9.0 - The engine vibrates and plays a loud sound when it's damaged to indicate more clearly that it needs repairs. - Added animated lights to alarm buzzers and sirens. - Sonar beacon's label can be edited in-game. +- Assistants gain skills faster than other characters. +- Skills increase faster during non-campaign rounds. +- More noticeable particle effects on damaged devices. +- Added "itemdamage" parameter to the explosion command. +- Huskified characters turn to the final stage faster. +- Using the "dumptofile" and "findentityids" commands doesn't require a permission from the server. +- Added Scale and Color properties to DecorativeSprites. +- Added OnDamaged status effect type. +- Added lights that indicate the state of a docking port. +- Significantly increased the item damages of all explosives. +- Replaced the old husk stinger with tentacles (similar to Husked Crawler). +- Minor adjustments to Husk, Human Husk, and Husked Crawler. +- Modding: Added "probabilitymultiplier" attribute for damagemodifiers. Can be used to make items/armor affect the probability of getting an affliction. +- Diving suits now give some protection against husk infections. +- Light components can be set to flicker in the sub editor. +- Fixed odd movements when pressing the ragdoll button while stunned. +- Made it easier to interact with doors that are overlapping with a docking port/hatch. +- Option to disable bot conversations in multiplayer in the server config file. + +Submarine editor improvements: +- Removed character mode. All the functionality of the character mode is now supported in the default mode. +- It's possible to modify the properties of multiple selected entities at the same time. +- Autosaving (the submarine is automatically saved to a temporary file which can be recovered if the game crashes). +- Added hotkeys for a bunch of actions (the hotkeys are visible in the tooltips). +- Pressing F focuses the camera on the selected entities. +- Control + A to select/deselect everything. +- Hold shift to ignore the grid when placing / resizing. +- The outlines of all wires are shown in wiring mode. +- Orphaned wires are deleted automatically. +- The content of the search box isn't reset when switching entity categories. +- Mouse middle mouse dragging is now 1:1, previously the view moved too fast. +- Linked submarines now have visuals when dragging. +- Holding down arrow keys now continues to move the entity after a small delay. +- Changing the background color by shift right clicking. + +Antigriefing improvements: +- Players can be kicked/banned/muted/votekicked and their ranks changed by right clicking the name in the crew list or chat. +- Karma penalty for stunning: gets progressively more severe the more stuns a player causes. +- Stealing weapons or ID cards from stunned/unconscious characters reduces karma. +- Added karma category to server log. + +Tab menu improvements: +- Improved layout. +- Show the roles of the players (moderator, admin, host) in the player list. +- Characters can be muted/kicked/banned from the player list. +- List the players who have joined/left the server. +- Display spectators in the list. +- Show player pings in the list. + +Command interface improvements: +- Contextual commands: characters can be ordered to operate/repair/use specific items by holding shift while enabling the command interface. +- Job icons are shown in the command interface to make it a little faster to differentiate between characters. +- Added separate orders for repairing electrical and mechanical devices. + +VOIP improvements: +- Added a keybind for local voice chat (= it's possible to only speak to players next to you without everyone within headset range hearing it). +- Added an adjustable delay for cutting audio capture after the push-to-talk key has been released. +- Fixed audio being suppressed when someone speaks even if VOIP volume is low or completely muted. Workshop improvements: - "Enabling" a mod through the ingame Workshop screen is no longer a thing; subscribing to a mod is all you need to do for the game to install it once it's downloaded. @@ -25,7 +203,7 @@ Workshop improvements: Bugfixes: - Fixed achievements not unlocking. -- Fixed positions of artifacts spawning on in caves and on level walls getting desynced between the server and clients. +- Fixed positions of artifacts spawning in caves and on level walls getting desynced between the server and clients. - Fixed new wire node being created at an item client-side if connecting the wire fails due to an electric shock server-side. - Fixed clients executing console commands they don't have permission to use. - Fixed enablecheats command not being relayed to server. @@ -36,6 +214,48 @@ Bugfixes: - Fixed searchlight toggle doing nothing. - Fixed hulls that have minuscule amounts of water in them (too small to be even rendered) being able to trigger InWater effects and water footstep sounds. - Fixed pumps dicarding the previously received set_targetlevel signal after 0.1 seconds, preventing manual control systems from working if the pumps aren't receiving a continuous set_targetlevel signal. +- Fixes to occasional crashes when rendering alien artifacts. +- Fixed radio chat key not working. +- Fixed reconnected clients not gaining XP. +- Fixes to sprite depth issues in Berilia's decorative fin structures. +- Fixed bots with an active order not switching to idle state when they have nothing to do, causing them to stand in place or walk against a wall. +- Fixed docking ports without a door sometimes getting linked to an incorrect door, preventing the door's linked gap from working correctly. +- Fixed monsters not spawning in ruins if the submarine hasn't left the starting outpost. +- Fixed freezing caused by SoundManager.InitStreamThread. +- Fixed generic Powered components (= charging dock) always using the default power consumption value defined in XML even if the power consumption is changed in the sub editor. +- Fixed last traitor objective's end message not being shown. +- Fixed command interface showing non-interactable devices as valid targets. +- Fixed dragged characters sometimes getting stuck on staircases. +- Fixed serious performance issues triggered by bot's combat and rescue objectives when there are no safe hulls left. +- Fixed characters being grabbable through walls. +- Potentially fixed a crash in GameMain.WindowActive caused by voice chat capture when the game is exiting. +- Spawn cargo above the floor structure of the cargo room, not above the bottom of the cargo hull. Fixes items spawning partially inside structures where the hull extends below the floor. +- Fixed all monsters bleeding red blood. +- Fixed light sprite's scale not being taken into account in light culling, causing lights with a large scaled-up light sprite to disappear before they're off-screen. +- Fixed damageable items not taking damage from repair tools. +- Fixed hitscan projectiles not hitting items. +- Fixed the short freeze when switching to the sub editor. +- Fixed sprites not being included in the xml element when using "copy to clipboard" in the particle editor. +- Fixed clients being able to use the number inputs in the multiplayer campaign store without the appropriate permissions. +- Fixed ability to buy more than 100 items of a kind despite 100 being the limit of how many purchased items of a kind can spawn. +- Fixed handheld sonar pinging when LMB is held, quickly draining the battery. +- Added some checks to prevent character sounds from crashing the game when audio playback is disabled. +- Fixed character collider's angular damping getting set to 0 if the character gets frozen, which caused the character's swimming animation to get wobbly. +- Fixed private messages not having the [PM] tag when dead. +- Fixed inability to open the health interface when hovering the cursor over another character, even if the character's health interface is inaccessible. +- Fixed bots being inactive when far away from all player characters. +- Fixed some particles (like muzzle flash) not being drawn on top of structures that are outside hulls. +- Fixed enemies being unable to target entities outside the submarine if they are inside it. +- Fixed oxygenite shards and tanks causing characters to move at turbo speed. +- Fixed waypoint links to gaps, doors, and ladders etc being removed when linking waypoint to another in the sub editor. Allow to remove links between waypoints. +- Fixed "teleportsub" console command teleporting also the connected outposts. +- Fixed characters getting stuck on platforms that extend outside the sub. +- Fixed diving suit lockers that have been recolored in the sub editor switching back to the default color when a suit is placed inside them. +- Coilgun/railgun loaders don't deteriorate if launching the turret fails. Fixes loaders deteriorating rapidly if the turret is receiving a continuous signal. +- Fixed rendering glitches on the surface of water when there's steep angles on the surface. +- Fixed inability to fire ranged reapons from sub to another through docking ports. +- Fixed inability drag characters from sub to another through docking ports. +- Fixed bots trying to shoot targets inside the alien ruins. --------------------------------------------------------------------------------------------------------- v0.9.8.0 @@ -129,7 +349,6 @@ Bugfixes: - Fixed all enemies ignoring the speed modifiers. - Fixed wires appearing as loose items on the floor after saving and reloading if they've been disconnected from both ends. - Fixed non-stackable status effects with a duration having no effect. ->>>>>>> origin/feature/carrier --------------------------------------------------------------------------------------------------------- v0.9.7.1 diff --git a/Barotrauma/BarotraumaShared/config.xml b/Barotrauma/BarotraumaShared/config.xml index b3f624337..9baa48f85 100644 --- a/Barotrauma/BarotraumaShared/config.xml +++ b/Barotrauma/BarotraumaShared/config.xml @@ -31,7 +31,7 @@ Down="S" Left="A" Right="D" - Attack="MiddleMouse" + Attack="R" Run="LeftShift" Crouch="LeftControl" InfoTab="Tab" diff --git a/Barotrauma/BarotraumaShared/serversettings.xml b/Barotrauma/BarotraumaShared/serversettings.xml index fb58fd6df..4830b170b 100644 --- a/Barotrauma/BarotraumaShared/serversettings.xml +++ b/Barotrauma/BarotraumaShared/serversettings.xml @@ -46,7 +46,7 @@ kickafktime="600" traitoruseratio="True" traitorratio="0.2" - karmaenabled="False" + karmaenabled="True" karmapreset="default" gamemodeidentifier="sandbox" missiontype="Random" diff --git a/Libraries/Facepunch.Steamworks/Enum/DebugOutputType.cs b/Libraries/Facepunch.Steamworks/Enum/DebugOutputType.cs index 5f36bbdee..d94bb6361 100644 --- a/Libraries/Facepunch.Steamworks/Enum/DebugOutputType.cs +++ b/Libraries/Facepunch.Steamworks/Enum/DebugOutputType.cs @@ -1,6 +1,6 @@ namespace Steamworks.Data { - enum DebugOutputType : int + public enum DebugOutputType : int { None = 0, Bug = 1, // You used the API incorrectly, or an internal error happened diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingUtils.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingUtils.cs index 8da70497d..8f168e163 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingUtils.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingUtils.cs @@ -204,11 +204,11 @@ namespace Steamworks #region FunctionMeta [UnmanagedFunctionPointer( Platform.MemberConvention )] - private delegate void FSetDebugOutputFunction( IntPtr self, DebugOutputType eDetailLevel, FSteamNetworkingSocketsDebugOutput pfnFunc ); + private delegate void FSetDebugOutputFunction( IntPtr self, DebugOutputType eDetailLevel, IntPtr pfnFunc ); private FSetDebugOutputFunction _SetDebugOutputFunction; #endregion - internal void SetDebugOutputFunction( DebugOutputType eDetailLevel, FSteamNetworkingSocketsDebugOutput pfnFunc ) + internal void SetDebugOutputFunction( DebugOutputType eDetailLevel, IntPtr pfnFunc ) { _SetDebugOutputFunction( Self, eDetailLevel, pfnFunc ); } diff --git a/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs b/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs index 6ea3cdc45..1c7a09c8e 100644 --- a/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs +++ b/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs @@ -81,6 +81,11 @@ namespace Steamworks public static long LocalTimestamp => Internal.GetLocalTimestamp(); + public static void SetDebugOutputFunction(DebugOutputType eDetailLevel, IntPtr pfnFunc) + { + Internal.SetDebugOutputFunction(eDetailLevel, pfnFunc); + } + /// /// [0 - 100] - Randomly discard N pct of packets diff --git a/Libraries/Facepunch.Steamworks/Structs/SteamNetworking.cs b/Libraries/Facepunch.Steamworks/Structs/SteamNetworking.cs index 3b8f78673..22868b48c 100644 --- a/Libraries/Facepunch.Steamworks/Structs/SteamNetworking.cs +++ b/Libraries/Facepunch.Steamworks/Structs/SteamNetworking.cs @@ -5,7 +5,8 @@ using System.Text; namespace Steamworks.Data { - delegate void FSteamNetworkingSocketsDebugOutput (DebugOutputType nType, string pszMsg ); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void FSteamNetworkingSocketsDebugOutput (DebugOutputType nType, string pszMsg ); public struct SteamNetworkingPOPID { diff --git a/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.cs b/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.cs index 7393a2401..88be2106d 100644 --- a/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.cs +++ b/Libraries/Farseer Physics Engine 3.5/Dynamics/Body.cs @@ -664,7 +664,7 @@ namespace FarseerPhysics.Dynamics } } - if (Enabled) + if (Enabled && World != null) { IBroadPhase broadPhase = World.ContactManager.BroadPhase; fixture.DestroyProxies(broadPhase); @@ -677,7 +677,7 @@ namespace FarseerPhysics.Dynamics ((PolygonShape)fixture.Shape).Vertices.AttachedToBody = false; #endif - if (World.FixtureRemoved != null) + if (World?.FixtureRemoved != null) World.FixtureRemoved(World, this, fixture); ResetMassData(); diff --git a/Libraries/GameAnalytics/GA_SDK_NETSTANDARD/GA_SDK_NETSTANDARD.csproj b/Libraries/GameAnalytics/GA_SDK_NETSTANDARD/GA_SDK_NETSTANDARD.csproj index 5fcec6050..cd6803b0e 100644 --- a/Libraries/GameAnalytics/GA_SDK_NETSTANDARD/GA_SDK_NETSTANDARD.csproj +++ b/Libraries/GameAnalytics/GA_SDK_NETSTANDARD/GA_SDK_NETSTANDARD.csproj @@ -26,7 +26,7 @@ - + diff --git a/Libraries/Lidgren.Network/NetPeer.Internal.cs b/Libraries/Lidgren.Network/NetPeer.Internal.cs index 8375dca6e..d2765ade2 100644 --- a/Libraries/Lidgren.Network/NetPeer.Internal.cs +++ b/Libraries/Lidgren.Network/NetPeer.Internal.cs @@ -132,7 +132,7 @@ namespace Lidgren.Network m_socket.ReceiveBufferSize = m_configuration.ReceiveBufferSize; m_socket.SendBufferSize = m_configuration.SendBufferSize; m_socket.Blocking = false; - m_socket.DualMode = true; + m_socket.DualMode = m_configuration.UseDualModeSockets; var ep = (EndPoint)new NetEndPoint(m_configuration.LocalAddress.MapToIPv6(), reBind ? m_listenPort : m_configuration.Port); m_socket.Bind(ep); diff --git a/Libraries/Lidgren.Network/NetPeerConfiguration.cs b/Libraries/Lidgren.Network/NetPeerConfiguration.cs index 56836fef7..b714edae2 100644 --- a/Libraries/Lidgren.Network/NetPeerConfiguration.cs +++ b/Libraries/Lidgren.Network/NetPeerConfiguration.cs @@ -142,6 +142,12 @@ namespace Lidgren.Network get { return m_appIdentifier; } } + public bool UseDualModeSockets + { + get; + set; + } = true; + /// /// Enables receiving of the specified type of message /// 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 c5fef7f3d..0812ff629 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/GraphicsDevice.DirectX.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/GraphicsDevice.DirectX.cs @@ -1382,13 +1382,13 @@ namespace Microsoft.Xna.Framework.Graphics private void PlatformDrawIndexedPrimitives(PrimitiveType primitiveType, int baseVertex, int startIndex, int primitiveCount) { + var indexCount = GetElementCountArray(primitiveType, primitiveCount); + lock (_d3dContext) { ApplyState(true); _d3dContext.InputAssembler.PrimitiveTopology = ToPrimitiveTopology(primitiveType); - - var indexCount = GetElementCountArray(primitiveType, primitiveCount); _d3dContext.DrawIndexed(indexCount, startIndex, baseVertex); } } diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs index 983a84dd7..4b12f5c1f 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs @@ -238,6 +238,44 @@ namespace Microsoft.Xna.Framework.Graphics } } + public void Draw(Texture2D texture, VertexPositionColorTexture[] vertices, float layerDepth) + { + CheckValid(texture); + + float sortKey = 0f; + + // set SortKey based on SpriteSortMode. + switch (_sortMode) + { + // Comparison of Texture objects. + case SpriteSortMode.Texture: + sortKey = texture.SortingKey; + break; + // Comparison of Depth + case SpriteSortMode.FrontToBack: + sortKey = layerDepth; + break; + // Comparison of Depth in reverse + case SpriteSortMode.BackToFront: + sortKey = -layerDepth; + break; + } + + int iters = vertices.Length / 4; + for (int i=0;i /// Submit a sprite for drawing in the current batch. /// @@ -1220,6 +1258,7 @@ namespace Microsoft.Xna.Framework.Graphics } } } + //_batcher.Dispose(); base.Dispose(disposing); } } diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatchItem.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatchItem.cs index 68e23646c..d0ebb45e5 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatchItem.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatchItem.cs @@ -22,7 +22,7 @@ namespace Microsoft.Xna.Framework.Graphics vertexBL = new VertexPositionColorTexture(); vertexBR = new VertexPositionColorTexture(); } - + public void Set ( float x, float y, float dx, float dy, float w, float h, float sin, float cos, Color color, Vector2 texCoordTL, Vector2 texCoordBR, float depth ) { // TODO, Should we be just assigning the Depth Value to Z? diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatcher.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatcher.cs index 7dd5cc934..036fd783c 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatcher.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatcher.cs @@ -14,7 +14,7 @@ namespace Microsoft.Xna.Framework.Graphics /// sent to the GPU). /// internal class SpriteBatcher - { + { /* * Note that this class is fundamental to high performance for SpriteBatch games. Please exercise * caution when making changes to this class. @@ -41,7 +41,7 @@ namespace Microsoft.Xna.Framework.Graphics /// Index pointer to the next available SpriteBatchItem in _batchItemList. /// private int _batchItemCount; - + /// /// The target graphics device. /// @@ -54,18 +54,18 @@ namespace Microsoft.Xna.Framework.Graphics private VertexPositionColorTexture[] _vertexArray; - public SpriteBatcher (GraphicsDevice device) - { + public SpriteBatcher(GraphicsDevice device) + { _device = device; - _batchItemList = new SpriteBatchItem[InitialBatchSize]; + _batchItemList = new SpriteBatchItem[InitialBatchSize]; _batchItemCount = 0; for (int i = 0; i < InitialBatchSize; i++) _batchItemList[i] = new SpriteBatchItem(); EnsureArrayCapacity(InitialBatchSize); - } + } /// /// Reuse a previously allocated SpriteBatchItem from the item pool. @@ -77,11 +77,11 @@ namespace Microsoft.Xna.Framework.Graphics if (_batchItemCount >= _batchItemList.Length) { var oldSize = _batchItemList.Length; - var newSize = oldSize + oldSize/2; // grow by x1.5 + var newSize = oldSize + oldSize / 2; // grow by x1.5 newSize = (newSize + 63) & (~63); // grow in chunks of 64. Array.Resize(ref _batchItemList, newSize); - for(int i=oldSize; i /// Sorts the batch items and then groups batch drawing into maximal allowed batch sets that do not /// overflow the 16 bit array indices for vertices. @@ -145,36 +145,36 @@ namespace Microsoft.Xna.Framework.Graphics /// The type of depth sorting desired for the rendering. /// The custom effect to apply to the drawn geometry public unsafe void DrawBatch(SpriteSortMode sortMode, Effect effect) - { + { if (effect != null && effect.IsDisposed) throw new ObjectDisposedException("effect"); - // nothing to do + // nothing to do if (_batchItemCount == 0) - return; - - // sort the batch items - switch ( sortMode ) - { - case SpriteSortMode.Texture : - case SpriteSortMode.FrontToBack : - case SpriteSortMode.BackToFront : - Array.Sort(_batchItemList, 0, _batchItemCount); - break; - } + return; + + // sort the batch items + switch (sortMode) + { + case SpriteSortMode.Texture: + case SpriteSortMode.FrontToBack: + case SpriteSortMode.BackToFront: + Array.Sort(_batchItemList, 0, _batchItemCount); + break; + } // Determine how many iterations through the drawing code we need to make int batchIndex = 0; int batchCount = _batchItemCount; - + unchecked { _device._graphicsMetrics._spriteCount += batchCount; } // Iterate through the batches, doing short.MaxValue sets of vertices only. - while(batchCount > 0) + while (batchCount > 0) { // setup the vertexArray array var startIndex = 0; @@ -208,10 +208,10 @@ namespace Microsoft.Xna.Framework.Graphics } // store the SpriteBatchItem data in our vertexArray - *(vertexArrayPtr+0) = item.vertexTL; - *(vertexArrayPtr+1) = item.vertexTR; - *(vertexArrayPtr+2) = item.vertexBL; - *(vertexArrayPtr+3) = item.vertexBR; + *(vertexArrayPtr + 0) = item.vertexTL; + *(vertexArrayPtr + 1) = item.vertexTR; + *(vertexArrayPtr + 2) = item.vertexBL; + *(vertexArrayPtr + 3) = item.vertexBR; // Release the texture. item.Texture = null; @@ -225,7 +225,7 @@ namespace Microsoft.Xna.Framework.Graphics } // return items to the pool. _batchItemCount = 0; - } + } /// /// Sends the triangle list to the graphics device. Here is where the actual drawing starts. @@ -278,6 +278,5 @@ namespace Microsoft.Xna.Framework.Graphics VertexPositionColorTexture.VertexDeclaration); } } - } + } } - 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 2cce0c726..93f4c9363 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/Texture2D.DirectX.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/Texture2D.DirectX.cs @@ -74,7 +74,10 @@ namespace Microsoft.Xna.Framework.Graphics lock (d3dContext) { d3dContext.UpdateSubresource(GetTexture(), subresourceIndex, region, dataPtr, GetPitch(w), 0); - d3dContext.GenerateMips(GetShaderResourceView()); + if (_mipmap) + { + d3dContext.GenerateMips(GetShaderResourceView()); + } } } finally diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Windows.NetStandard.csproj b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Windows.NetStandard.csproj index 0b4d267a9..a98cc5944 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Windows.NetStandard.csproj +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Windows.NetStandard.csproj @@ -500,19 +500,18 @@ + + + + - - - - - diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Properties/launchSettings.json b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Properties/launchSettings.json new file mode 100644 index 000000000..42118d872 --- /dev/null +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "MonoGame.Framework.Windows.NetStandard": { + "commandName": "Project", + "nativeDebugging": false + } + } +} \ No newline at end of file