diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index f1dd4470c..83e1255cb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -1156,9 +1156,9 @@ namespace Barotrauma } } - partial void OnTalentGiven(string talentIdentifier) + partial void OnTalentGiven(TalentPrefab talentPrefab) { - AddMessage(TextManager.Get("talentname." + talentIdentifier.ToString()), GUI.Style.Yellow, playSound: this == Controlled); + AddMessage(TextManager.Get("talentname." + talentPrefab.Identifier), GUI.Style.Yellow, playSound: this == Controlled); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index 3d42e6e3f..754c97f22 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -521,6 +521,7 @@ namespace Barotrauma ch.SkinColor = skinColor; ch.HairColor = hairColor; ch.FacialHairColor = facialHairColor; + ch.SetPersonalityTrait(); if (ch.Job != null) { foreach (KeyValuePair skill in skillLevels) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs index f4a5f282c..73c444116 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/AfflictionHusk.cs @@ -2,12 +2,19 @@ { partial class AfflictionHusk : Affliction { + private InfectionState? prevDisplayedMessage; partial void UpdateMessages() { if (Prefab is AfflictionPrefabHusk { SendMessages: false }) { return; } + if (prevDisplayedMessage.HasValue && prevDisplayedMessage.Value == State) { return; } + switch (State) { case InfectionState.Dormant: + if (Strength < DormantThreshold * 0.5f) + { + return; + } GUI.AddMessage(TextManager.Get("HuskDormant"), GUI.Style.Red); break; case InfectionState.Transition: @@ -23,6 +30,7 @@ default: break; } + prevDisplayedMessage = State; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index 2660e8a10..acf9b7fd7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -531,6 +531,8 @@ namespace Barotrauma bloodParticleTimer -= deltaTime * (affliction.Strength / 10.0f); if (bloodParticleTimer <= 0.0f) { + Limb limb = targetLimb ?? Character.AnimController.MainLimb; + bool inWater = Character.AnimController.InWater; var drawTarget = inWater ? Particles.ParticlePrefab.DrawTargetType.Water : Particles.ParticlePrefab.DrawTargetType.Air; var emitter = Character.BloodEmitters.FirstOrDefault(e => e.Prefab.ParticlePrefab.DrawTarget == drawTarget || e.Prefab.ParticlePrefab.DrawTarget == Particles.ParticlePrefab.DrawTargetType.Both); @@ -543,13 +545,13 @@ namespace Barotrauma if (!inWater) { bloodParticleSize *= 2.0f; - velocity = targetLimb.LinearVelocity * 100.0f; + velocity = limb.LinearVelocity * 100.0f; } // TODO: use the blood emitter? var blood = GameMain.ParticleManager.CreateParticle( inWater ? Character.Params.BleedParticleWater : Character.Params.BleedParticleAir, - targetLimb.WorldPosition, velocity, 0.0f, Character.AnimController.CurrentHull); + limb.WorldPosition, velocity, 0.0f, Character.AnimController.CurrentHull); if (blood != null && !inWater) { @@ -1122,23 +1124,24 @@ namespace Barotrauma } public static Color GetAfflictionIconColor(AfflictionPrefab prefab, Affliction affliction) + { + return GetAfflictionIconColor(prefab, affliction.Strength); + } + + public static Color GetAfflictionIconColor(AfflictionPrefab prefab, float afflictionStrength) { // No specific colors, use generic if (prefab.IconColors == null) { if (prefab.IsBuff) { - return ToolBox.GradientLerp(affliction.Strength / prefab.MaxStrength, GUI.Style.BuffColorLow, GUI.Style.BuffColorMedium, GUI.Style.BuffColorHigh); - } - else - { - return ToolBox.GradientLerp(affliction.Strength / prefab.MaxStrength, GUI.Style.DebuffColorLow, GUI.Style.DebuffColorMedium, GUI.Style.DebuffColorHigh); + return ToolBox.GradientLerp(afflictionStrength / prefab.MaxStrength, GUI.Style.BuffColorLow, GUI.Style.BuffColorMedium, GUI.Style.BuffColorHigh); } + + return ToolBox.GradientLerp(afflictionStrength / prefab.MaxStrength, GUI.Style.DebuffColorLow, GUI.Style.DebuffColorMedium, GUI.Style.DebuffColorHigh); } - else - { - return ToolBox.GradientLerp(affliction.Strength / prefab.MaxStrength, prefab.IconColors); - } + + return ToolBox.GradientLerp(afflictionStrength / prefab.MaxStrength, prefab.IconColors); } public static Color GetAfflictionIconColor(Affliction affliction) => GetAfflictionIconColor(affliction.Prefab, affliction); @@ -1153,7 +1156,7 @@ namespace Barotrauma return; } - if (afflictionsDirty()) + if (afflictionsDirty() || selectedLimb != currentDisplayedLimb) { var currentAfflictions = afflictions.Where(a => ShouldDisplayAfflictionOnLimb(a, selectedLimb)).Select(a => a.Key); CreateAfflictionInfos(currentAfflictions); diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 14119e426..7498d3d57 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -1120,7 +1120,7 @@ namespace Barotrauma return; } - if (Submarine.MainSub.SaveAs(Barotrauma.IO.Path.Combine(SubmarineInfo.SavePath, fileName + ".sub"))) + if (Submarine.MainSub.TrySaveAs(Barotrauma.IO.Path.Combine(SubmarineInfo.SavePath, fileName + ".sub"))) { NewMessage("Sub saved", Color.Green); } @@ -2392,8 +2392,9 @@ namespace Barotrauma commands.Add(new Command("querylobbies", "Queries all SteamP2P lobbies", (args) => { TaskPool.Add("DebugQueryLobbies", - SteamManager.LobbyQueryRequest(), (t) => { - var lobbies = ((Task>)t).Result; + SteamManager.LobbyQueryRequest(), (t) => + { + t.TryGetResult(out List lobbies); foreach (var lobby in lobbies) { NewMessage(lobby.GetData("name") + ", " + lobby.GetData("lobbyowner"), Color.Yellow); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/ScanMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/ScanMission.cs index 93dadacc4..c7488925c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/ScanMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/ScanMission.cs @@ -60,7 +60,16 @@ namespace Barotrauma ushort id = msg.ReadUInt16(); bool scanned = msg.ReadBoolean(); Entity entity = Entity.FindEntityByID(id); - scanTargets.Add(entity as WayPoint, scanned); + if (!(entity is WayPoint wayPoint)) + { + string errorMsg = $"Failed to find a waypoint in ScanMission.ClientReadScanTargetStatus. Entity {id} was {(entity?.ToString() ?? null)}"; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("ScanMission.ClientReadScanTargetStatus", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); + } + else + { + scanTargets.Add(wayPoint, scanned); + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs index aff1e4c05..b2540f17c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs @@ -1,8 +1,10 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using SharpFont; using System; using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; namespace Barotrauma @@ -130,7 +132,7 @@ namespace Barotrauma /// Character ranges between each even element with their corresponding odd element. Default is 0x20 to 0xFFFF. /// Texture dimensions. Default is 512x512. /// Base character used to shift all other characters downwards when rendering. Defaults to T. - public void RenderAtlas(GraphicsDevice gd, uint[] charRanges = null, int texDims = 1024, uint baseChar = 0x54) + private void RenderAtlas(GraphicsDevice gd, uint[] charRanges = null, int texDims = 1024, uint baseChar = 0x54) { if (DynamicLoading) { return; } @@ -253,13 +255,19 @@ namespace Barotrauma } } - public void DynamicRenderAtlas(GraphicsDevice gd, uint character, int texDims = 1024, uint baseChar = 0x54) + private void DynamicRenderAtlas(GraphicsDevice gd, uint character, int texDims = 1024, uint baseChar = 0x54) + => DynamicRenderAtlas(gd, character.ToEnumerable(), texDims, baseChar); + + private void DynamicRenderAtlas(GraphicsDevice gd, string str, int texDims = 1024, uint baseChar = 0x54) + => DynamicRenderAtlas(gd, str.Distinct().Select(c => (uint)c), texDims, baseChar); + + private void DynamicRenderAtlas(GraphicsDevice gd, IEnumerable characters, int texDims = 1024, uint baseChar = 0x54) { if (System.Threading.Thread.CurrentThread != GameMain.MainThread) { CrossThread.RequestExecutionOnMainThread(() => { - DynamicRenderAtlas(gd, character, texDims, baseChar); + DynamicRenderAtlas(gd, characters, texDims, baseChar); }); return; } @@ -271,7 +279,6 @@ namespace Barotrauma lock (mutex) { - if (texCoords.ContainsKey(character)) { return; } if (textures.Count == 0) { this.texDims = texDims; @@ -282,79 +289,90 @@ namespace Barotrauma textures.Add(new Texture2D(gd, texDims, texDims, false, SurfaceFormat.Color)); } - uint glyphIndex = face.GetCharIndex(character); - if (glyphIndex == 0) { return; } - - face.SetPixelSizes(0, size); - face.LoadGlyph(glyphIndex, LoadFlags.Default, LoadTarget.Normal); - if (face.Glyph.Metrics.Width == 0 || face.Glyph.Metrics.Height == 0) + bool anyChanges = false; + bool firstChar = true; + foreach (var character in characters) { - if (face.Glyph.Metrics.HorizontalAdvance > 0) + if (texCoords.ContainsKey(character)) { continue; } + + uint glyphIndex = face.GetCharIndex(character); + if (glyphIndex == 0) { continue; } + + face.SetPixelSizes(0, size); + face.LoadGlyph(glyphIndex, LoadFlags.Default, LoadTarget.Normal); + if (face.Glyph.Metrics.Width == 0 || face.Glyph.Metrics.Height == 0) { - //glyph is empty, but char still applies advance - GlyphData blankData = new GlyphData( - advance: (float)face.Glyph.Metrics.HorizontalAdvance, - texIndex: -1); //indicates no texture because the glyph is empty - texCoords.Add(character, blankData); + if (face.Glyph.Metrics.HorizontalAdvance > 0) + { + //glyph is empty, but char still applies advance + GlyphData blankData = new GlyphData( + advance: (float)face.Glyph.Metrics.HorizontalAdvance, + texIndex: -1); //indicates no texture because the glyph is empty + texCoords.Add(character, blankData); + } + continue; } - return; - } - //stacktrace doesn't really work that well when RenderGlyph throws an exception - face.Glyph.RenderGlyph(RenderMode.Normal); - bitmap = (byte[])face.Glyph.Bitmap.BufferData.Clone(); - glyphWidth = face.Glyph.Bitmap.Width; - glyphHeight = bitmap.Length / glyphWidth; - horizontalAdvance = face.Glyph.Metrics.HorizontalAdvance; - drawOffset = new Vector2(face.Glyph.BitmapLeft, baseHeight * 14 / 10 - face.Glyph.BitmapTop); - - if (glyphWidth > texDims - 1 || glyphHeight > texDims - 1) - { - throw new Exception(filename + ", " + size.ToString() + ", " + (char)character + "; Glyph dimensions exceed texture atlas dimensions"); - } - - currentDynamicAtlasNextY = Math.Max(currentDynamicAtlasNextY, glyphHeight + 2); - if (currentDynamicAtlasCoords.X + glyphWidth + 2 > texDims - 1) - { - currentDynamicAtlasCoords.X = 0; - currentDynamicAtlasCoords.Y += currentDynamicAtlasNextY; - currentDynamicAtlasNextY = 0; - } - //no more room in current texture atlas, create a new one - if (currentDynamicAtlasCoords.Y + glyphHeight + 2 > texDims - 1) - { - currentDynamicAtlasCoords.X = 0; - currentDynamicAtlasCoords.Y = 0; - currentDynamicAtlasNextY = 0; - textures.Add(new Texture2D(gd, texDims, texDims, false, SurfaceFormat.Color)); - currentDynamicPixelBuffer = null; - } + //stacktrace doesn't really work that well when RenderGlyph throws an exception + face.Glyph.RenderGlyph(RenderMode.Normal); + bitmap = (byte[])face.Glyph.Bitmap.BufferData.Clone(); + glyphWidth = face.Glyph.Bitmap.Width; + glyphHeight = bitmap.Length / glyphWidth; + horizontalAdvance = face.Glyph.Metrics.HorizontalAdvance; + drawOffset = new Vector2(face.Glyph.BitmapLeft, baseHeight * 14 / 10 - face.Glyph.BitmapTop); - GlyphData newData = new GlyphData( - advance: (float)horizontalAdvance, - texIndex: textures.Count - 1, - texCoords: new Rectangle((int)currentDynamicAtlasCoords.X, (int)currentDynamicAtlasCoords.Y, glyphWidth, glyphHeight), - drawOffset: drawOffset - ); - texCoords.Add(character, newData); - - if (currentDynamicPixelBuffer == null) - { - currentDynamicPixelBuffer = new uint[texDims * texDims]; - textures[newData.TexIndex].GetData(currentDynamicPixelBuffer, 0, texDims * texDims); - } - - for (int y = 0; y < glyphHeight; y++) - { - for (int x = 0; x < glyphWidth; x++) + if (glyphWidth > texDims - 1 || glyphHeight > texDims - 1) { - byte byteColor = bitmap[x + y * glyphWidth]; - currentDynamicPixelBuffer[((int)currentDynamicAtlasCoords.X + x) + ((int)currentDynamicAtlasCoords.Y + y) * texDims] = (uint)(byteColor << 24 | 0x00ffffff); + throw new Exception(filename + ", " + size.ToString() + ", " + (char)character + "; Glyph dimensions exceed texture atlas dimensions"); } - } - textures[newData.TexIndex].SetData(currentDynamicPixelBuffer); - currentDynamicAtlasCoords.X += glyphWidth + 2; + currentDynamicAtlasNextY = Math.Max(currentDynamicAtlasNextY, glyphHeight + 2); + if (currentDynamicAtlasCoords.X + glyphWidth + 2 > texDims - 1) + { + currentDynamicAtlasCoords.X = 0; + currentDynamicAtlasCoords.Y += currentDynamicAtlasNextY; + currentDynamicAtlasNextY = 0; + } + //no more room in current texture atlas, create a new one + if (currentDynamicAtlasCoords.Y + glyphHeight + 2 > texDims - 1) + { + if (!firstChar) { textures[^1].SetData(currentDynamicPixelBuffer); } + currentDynamicAtlasCoords.X = 0; + currentDynamicAtlasCoords.Y = 0; + currentDynamicAtlasNextY = 0; + textures.Add(new Texture2D(gd, texDims, texDims, false, SurfaceFormat.Color)); + currentDynamicPixelBuffer = null; + } + + GlyphData newData = new GlyphData( + advance: (float)horizontalAdvance, + texIndex: textures.Count - 1, + texCoords: new Rectangle((int)currentDynamicAtlasCoords.X, (int)currentDynamicAtlasCoords.Y, glyphWidth, glyphHeight), + drawOffset: drawOffset + ); + texCoords.Add(character, newData); + + if (currentDynamicPixelBuffer == null) + { + currentDynamicPixelBuffer = new uint[texDims * texDims]; + textures[newData.TexIndex].GetData(currentDynamicPixelBuffer, 0, texDims * texDims); + } + + for (int y = 0; y < glyphHeight; y++) + { + for (int x = 0; x < glyphWidth; x++) + { + byte byteColor = bitmap[x + y * glyphWidth]; + currentDynamicPixelBuffer[((int)currentDynamicAtlasCoords.X + x) + ((int)currentDynamicAtlasCoords.Y + y) * texDims] = (uint)(byteColor << 24 | 0x00ffffff); + } + } + + currentDynamicAtlasCoords.X += glyphWidth + 2; + firstChar = false; + anyChanges = true; + } + + if (anyChanges) { textures[^1].SetData(currentDynamicPixelBuffer); } } } @@ -374,6 +392,10 @@ namespace Barotrauma public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth) { if (textures.Count == 0 && !DynamicLoading) { return; } + if (DynamicLoading) + { + DynamicRenderAtlas(graphicsDevice, text); + } int lineNum = 0; Vector2 currentPos = position; @@ -390,10 +412,6 @@ namespace Barotrauma } uint charIndex = text[i]; - if (DynamicLoading) - { - DynamicRenderAtlas(graphicsDevice, charIndex); - } GlyphData gd = GetGlyphData(charIndex); if (gd.TexIndex >= 0) @@ -417,6 +435,10 @@ namespace Barotrauma public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color) { if (textures.Count == 0 && !DynamicLoading) { return; } + if (DynamicLoading) + { + DynamicRenderAtlas(graphicsDevice, text); + } Vector2 currentPos = position; for (int i = 0; i < text.Length; i++) @@ -429,10 +451,6 @@ namespace Barotrauma } uint charIndex = text[i]; - if (DynamicLoading) - { - DynamicRenderAtlas(graphicsDevice, charIndex); - } GlyphData gd = GetGlyphData(charIndex); if (gd.TexIndex >= 0) @@ -452,6 +470,10 @@ namespace Barotrauma public void DrawStringWithColors(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth, List richTextData, int rtdOffset = 0) { if (textures.Count == 0 && !DynamicLoading) { return; } + if (DynamicLoading) + { + DynamicRenderAtlas(graphicsDevice, text); + } int lineNum = 0; Vector2 currentPos = position; @@ -472,10 +494,6 @@ namespace Barotrauma } uint charIndex = text[i]; - if (DynamicLoading && !texCoords.ContainsKey(charIndex)) - { - DynamicRenderAtlas(graphicsDevice, charIndex); - } Color currentTextColor; @@ -626,6 +644,10 @@ namespace Barotrauma { retVal.Y = baseHeight; } + if (DynamicLoading) + { + DynamicRenderAtlas(graphicsDevice, text); + } for (int i = 0; i < text.Length; i++) { @@ -636,10 +658,6 @@ namespace Barotrauma continue; } uint charIndex = text[i]; - if (DynamicLoading && !texCoords.ContainsKey(charIndex)) - { - DynamicRenderAtlas(graphicsDevice, charIndex); - } GlyphData gd = GetGlyphData(charIndex); currentLineX += gd.Advance; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index 0ca71144c..5f6afa2e3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -44,12 +44,12 @@ namespace Barotrauma Waiting, // Hourglass WaitingBackground // Cursor + Hourglass } - + public static class GUI { public static GUICanvas Canvas => GUICanvas.Instance; public static CursorState MouseCursor = CursorState.Default; - + public static readonly SamplerState SamplerState = new SamplerState() { Filter = TextureFilter.Linear, @@ -116,14 +116,14 @@ namespace Barotrauma public static float SlicedSpriteScale { - get + get { - if (Math.Abs(1.0f - Scale) < 0.1f) - { + if (Math.Abs(1.0f - Scale) < 0.1f) + { //don't scale if very close to the "reference resolution" - return 1.0f; + return 1.0f; } - return Scale; + return Scale; } } @@ -306,7 +306,7 @@ namespace Barotrauma t = new Texture2D(GraphicsDevice, 1, 1); t.SetData(new Color[] { Color.White });// fill the texture with white }); - + SubmarineIcon = new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(452, 385, 182, 81), new Vector2(0.5f, 0.5f)); arrow = new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(393, 393, 49, 45), new Vector2(0.5f, 0.5f)); SpeechBubbleIcon = new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(385, 449, 66, 60), new Vector2(0.5f, 0.5f)); @@ -314,7 +314,7 @@ namespace Barotrauma } /// - /// By default, all the gui elements are drawn automatically in the same order they appear on the update list. + /// By default, all the gui elements are drawn automatically in the same order they appear on the update list. /// public static void Draw(Camera cam, SpriteBatch spriteBatch) { @@ -706,11 +706,11 @@ namespace Barotrauma spriteBatch.Begin(SpriteSortMode.Immediate, effect: GameMain.GameScreen.PostProcessEffect); float scale = Math.Max( - (float)GameMain.GraphicsWidth / backgroundSprite.SourceRect.Width, + (float)GameMain.GraphicsWidth / backgroundSprite.SourceRect.Width, (float)GameMain.GraphicsHeight / backgroundSprite.SourceRect.Height) * 1.1f; float paddingX = backgroundSprite.SourceRect.Width * scale - GameMain.GraphicsWidth; float paddingY = backgroundSprite.SourceRect.Height * scale - GameMain.GraphicsHeight; - + double noiseT = (Timing.TotalTime * 0.02f); Vector2 pos = new Vector2((float)PerlinNoise.CalculatePerlin(noiseT, noiseT, 0) - 0.5f, (float)PerlinNoise.CalculatePerlin(noiseT, noiseT, 0.5f) - 0.5f); pos = new Vector2(pos.X * paddingX, pos.Y * paddingY); @@ -719,7 +719,7 @@ namespace Barotrauma new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) / 2 + pos, null, Color.White, 0.0f, backgroundSprite.size / 2, scale, SpriteEffects.None, 0.0f); - + spriteBatch.End(); } @@ -759,8 +759,8 @@ namespace Barotrauma else { additions.Enqueue(component); - } - } + } + } } /// @@ -786,7 +786,7 @@ namespace Barotrauma component.Children.ForEach(c => RemoveFromUpdateList(c)); } } - } + } } public static void ClearUpdateList() @@ -900,7 +900,7 @@ namespace Barotrauma { GUIMessageBox.VisibleBox.AddToGUIUpdateList(); } - } + } } #endregion @@ -941,7 +941,7 @@ namespace Barotrauma inventoryIndex = updateList.IndexOf(CharacterHUD.HUDFrame); } - if ((!PlayerInput.PrimaryMouseButtonHeld() && !PlayerInput.PrimaryMouseButtonClicked()) || + if ((!PlayerInput.PrimaryMouseButtonHeld() && !PlayerInput.PrimaryMouseButtonClicked()) || (prevMouseOn == null && !PlayerInput.SecondaryMouseButtonHeld() && !Inventory.DraggingItems.Any())) { for (var i = updateList.Count - 1; i > inventoryIndex; i--) @@ -967,7 +967,7 @@ namespace Barotrauma return MouseOn; } } - + private static CursorState UpdateMouseCursorState(GUIComponent c) { lock (mutex) @@ -994,7 +994,7 @@ namespace Barotrauma } if (Wire.DraggingWire != null) { return CursorState.Dragging; } } - + if (c == null || c is GUICustomComponent) { switch (Screen.Selected) @@ -1027,7 +1027,7 @@ namespace Barotrauma } } } - + if (c != null && c.Visible) { if (c.AlwaysOverrideCursor) { return c.HoverCursor; } @@ -1036,20 +1036,20 @@ namespace Barotrauma // And this is of course picked up as clickable area. // There has to be a better way of checking this but for now this works. var monitorRect = new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight); - + var parent = FindInteractParent(c); - + if (c.Enabled) { // Some parent elements take priority // but not when the child is a GUIButton or GUITickBox - if (!(parent is GUIButton) && !(parent is GUIListBox) || + if (!(parent is GUIButton) && !(parent is GUIListBox) || (c is GUIButton) || (c is GUITickBox)) { if (!c.Rect.Equals(monitorRect)) { return c.HoverCursor; } } } - + // Children in list boxes can be interacted with despite not having // a GUIButton inside of them so instead of hard coding we check if // the children can be interacted with by checking their hover state @@ -1084,7 +1084,7 @@ namespace Barotrauma { // Health menus if (character.CharacterHealth.MouseOnElement) { return CursorState.Hand; } - + if (character.SelectedCharacter != null) { if (character.SelectedCharacter.CharacterHealth.MouseOnElement) @@ -1096,7 +1096,7 @@ namespace Barotrauma // Character is hovering over an item placed in the world if (character.FocusedItem != null) { return CursorState.Hand; } } - + return CursorState.Default; static GUIComponent FindInteractParent(GUIComponent component) @@ -1130,7 +1130,7 @@ namespace Barotrauma } } } - + static bool ContainsMouse(GUIComponent component) { // If component has a mouse rectangle then use that, if not use it's physical rect @@ -1138,7 +1138,7 @@ namespace Barotrauma component.MouseRect.Contains(PlayerInput.MousePosition) : component.Rect.Contains(PlayerInput.MousePosition); } - } + } } /// @@ -1153,8 +1153,8 @@ namespace Barotrauma { MouseCursor = CursorState.Waiting; var timeOut = DateTime.Now + new TimeSpan(0, 0, waitSeconds); - while (DateTime.Now < timeOut) - { + while (DateTime.Now < timeOut) + { if (endCondition != null) { try @@ -1163,13 +1163,13 @@ namespace Barotrauma } catch { break; } } - yield return CoroutineStatus.Running; + yield return CoroutineStatus.Running; } if (MouseCursor == CursorState.Waiting) { MouseCursor = CursorState.Default; } yield return CoroutineStatus.Success; } } - + public static void ClearCursorWait() { lock (mutex) @@ -1208,7 +1208,7 @@ namespace Barotrauma { debugDrawMetadataOffset--; } - + if (PlayerInput.KeyHit(Keys.Down)) { debugDrawMetadataOffset++; @@ -1240,17 +1240,20 @@ namespace Barotrauma debugDrawMetadataOffset = 0; } } - + } HandlePersistingElements(deltaTime); RefreshUpdateList(); UpdateMouseOn(); Debug.Assert(updateList.Count == updateListSet.Count); - updateList.ForEach(c => c.UpdateAuto(deltaTime)); + foreach (var c in updateList) + { + c.UpdateAuto(deltaTime); + } UpdateMessages(deltaTime); UpdateSavingIndicator(deltaTime); - } + } } private static void UpdateMessages(float deltaTime) @@ -1281,17 +1284,16 @@ namespace Barotrauma //only the first message (the currently visible one) is updated at a time break; } - + foreach (GUIMessage msg in messages) { if (!msg.WorldSpace) { continue; } - msg.Timer -= deltaTime; - msg.Pos += msg.Velocity * deltaTime; + msg.Timer -= deltaTime; + msg.Pos += msg.Velocity * deltaTime; } messages.RemoveAll(m => m.Timer <= 0.0f); } - } private static void UpdateSavingIndicator(float deltaTime) @@ -1628,7 +1630,7 @@ namespace Barotrauma private static void DrawMessages(SpriteBatch spriteBatch, Camera cam) { - if (messages.Count == 0) return; + if (messages.Count == 0) { return; } bool useScissorRect = messages.Any(m => !m.WorldSpace); Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle; @@ -1647,7 +1649,7 @@ namespace Barotrauma msg.Font.DrawString(spriteBatch, msg.Text, drawPos + msg.DrawPos + Vector2.One, Color.Black, 0, msg.Origin, 1.0f, SpriteEffects.None, 0); msg.Font.DrawString(spriteBatch, msg.Text, drawPos + msg.DrawPos, msg.Color, 0, msg.Origin, 1.0f, SpriteEffects.None, 0); - break; + break; } if (useScissorRect) @@ -1656,11 +1658,11 @@ namespace Barotrauma spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect; spriteBatch.Begin(SpriteSortMode.Deferred); } - + foreach (GUIMessage msg in messages) { if (!msg.WorldSpace) { continue; } - + if (cam != null) { float alpha = 1.0f; @@ -1669,7 +1671,7 @@ namespace Barotrauma Vector2 drawPos = cam.WorldToScreen(msg.DrawPos); msg.Font.DrawString(spriteBatch, msg.Text, drawPos + Vector2.One, Color.Black * alpha, 0, msg.Origin, 1.0f, SpriteEffects.None, 0); msg.Font.DrawString(spriteBatch, msg.Text, drawPos, msg.Color * alpha, 0, msg.Origin, 1.0f, SpriteEffects.None, 0); - } + } } messages.RemoveAll(m => m.Timer <= 0.0f); @@ -1770,7 +1772,7 @@ namespace Barotrauma { int textureWidth = Math.Max(radius * 2, 1); int textureHeight = Math.Max(height + radius * 2, 1); - + Color[] data = new Color[textureWidth * textureHeight]; // Colour the entire texture transparent first. @@ -1880,9 +1882,9 @@ namespace Barotrauma /// Creates multiple elements with relative size and positions them automatically. /// public static List CreateElements(int count, Vector2 relativeSize, RectTransform parent, Func constructor, - Anchor anchor = Anchor.TopLeft, Pivot? pivot = null, Point? minSize = null, Point? maxSize = null, - int absoluteSpacing = 0, float relativeSpacing = 0, Func extraSpacing = null, - int startOffsetAbsolute = 0, float startOffsetRelative = 0, bool isHorizontal = false) + Anchor anchor = Anchor.TopLeft, Pivot? pivot = null, Point? minSize = null, Point? maxSize = null, + int absoluteSpacing = 0, float relativeSpacing = 0, Func extraSpacing = null, + int startOffsetAbsolute = 0, float startOffsetRelative = 0, bool isHorizontal = false) where T : GUIComponent { return CreateElements(count, parent, constructor, relativeSize, null, anchor, pivot, minSize, maxSize, absoluteSpacing, relativeSpacing, extraSpacing, startOffsetAbsolute, startOffsetRelative, isHorizontal); @@ -1891,8 +1893,8 @@ namespace Barotrauma /// /// Creates multiple elements with absolute size and positions them automatically. /// - public static List CreateElements(int count, Point absoluteSize, RectTransform parent, Func constructor, - Anchor anchor = Anchor.TopLeft, Pivot? pivot = null, + public static List CreateElements(int count, Point absoluteSize, RectTransform parent, Func constructor, + Anchor anchor = Anchor.TopLeft, Pivot? pivot = null, int absoluteSpacing = 0, float relativeSpacing = 0, Func extraSpacing = null, int startOffsetAbsolute = 0, float startOffsetRelative = 0, bool isHorizontal = false) where T : GUIComponent @@ -1991,7 +1993,7 @@ namespace Barotrauma if (i == 0) numberInput.IntValue = value.X; else - numberInput.IntValue = value.Y; + numberInput.IntValue = value.Y; } return frame; } @@ -2028,6 +2030,16 @@ namespace Barotrauma return frame; } + public static void NotifyPrompt(string header, string body) + { + GUIMessageBox msgBox = new GUIMessageBox(header, body, new[] { TextManager.Get("Ok") }, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175)); + msgBox.Buttons[0].OnClicked = delegate + { + msgBox.Close(); + return true; + }; + } + public static GUIMessageBox AskForConfirmation(string header, string body, Action onConfirm, Action onDeny = null) { string[] buttons = { TextManager.Get("Ok"), TextManager.Get("Cancel") }; @@ -2051,6 +2063,32 @@ namespace Barotrauma return msgBox; } + public static GUIMessageBox PromptTextInput(string header, string body, Action onConfirm) + { + string[] buttons = { TextManager.Get("Ok"), TextManager.Get("Cancel") }; + GUIMessageBox msgBox = new GUIMessageBox(header, string.Empty, buttons, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175)); + GUITextBox textBox = new GUITextBox(new RectTransform(Vector2.One, msgBox.Content.RectTransform), text: body) + { + OverflowClip = true + }; + + // Cancel button + msgBox.Buttons[1].OnClicked = delegate + { + msgBox.Close(); + return true; + }; + + // Ok button + msgBox.Buttons[0].OnClicked = delegate + { + onConfirm.Invoke(textBox.Text); + msgBox.Close(); + return true; + }; + return msgBox; + } + #endregion #region Element positioning @@ -2210,7 +2248,7 @@ namespace Barotrauma if (disallowedAreas == null) { continue; } foreach (Rectangle rect2 in disallowedAreas) { - if (!rect1.Intersects(rect2)) { continue; } + if (!rect1.Intersects(rect2)) { continue; } intersections = true; Point centerDiff = rect1.Center - rect2.Center; @@ -2330,8 +2368,8 @@ namespace Barotrauma }); } - CreateButton(GameMain.GameSession.GameMode is CampaignMode ? "ReturnToServerlobby" : "EndRound", buttonContainer, - verificationTextTag: GameMain.GameSession.GameMode is CampaignMode ? "PauseMenuReturnToServerLobbyVerification" : "EndRoundSubNotAtLevelEnd", + CreateButton(GameMain.GameSession.GameMode is CampaignMode ? "ReturnToServerlobby" : "EndRound", buttonContainer, + verificationTextTag: GameMain.GameSession.GameMode is CampaignMode ? "PauseMenuReturnToServerLobbyVerification" : "EndRoundSubNotAtLevelEnd", action: () => { GameMain.Client?.RequestRoundEnd(save: false); @@ -2448,7 +2486,10 @@ namespace Barotrauma public static void ClearMessages() { - messages.Clear(); + lock (mutex) + { + messages.Clear(); + } } public static bool IsFourByThree() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs index 94a503f1e..52b34ece5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIButton.cs @@ -219,12 +219,6 @@ namespace Barotrauma GUI.Style.ButtonPulse.Draw(spriteBatch, expandRect, ToolBox.GradientLerp(pulseExpand, Color.White, Color.White, Color.Transparent)); } - - if (UserData is string s && s == "ReadyCheckButton" && ReadyCheck.lastReadyCheck > DateTime.Now) - { - float progress = (ReadyCheck.lastReadyCheck - DateTime.Now).Seconds / 60.0f; - Frame.Color = ToolBox.GradientLerp(progress, Color.White, GUI.Style.Red); - } } protected override void Update(float deltaTime) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index f03ad241a..6da7dc016 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -317,7 +317,10 @@ namespace Barotrauma set { selected = value; - Children.ForEach(c => c.Selected = value); + foreach (var child in Children) + { + child.Selected = value; + } } } public virtual ComponentState State @@ -537,7 +540,10 @@ namespace Barotrauma //would be real nice to un-jank this some day ForceUpdate(); ForceUpdate(); - foreach (var child in Children) { child.ForceLayoutRecalculation(); } + foreach (var child in Children) + { + child.ForceLayoutRecalculation(); + } } public void ForceUpdate() => Update((float)Timing.Step); @@ -547,7 +553,10 @@ namespace Barotrauma /// public void UpdateChildren(float deltaTime, bool recursive) { - RectTransform.Children.ForEach(c => c.GUIComponent.UpdateManually(deltaTime, recursive, recursive)); + foreach (var child in RectTransform.Children) + { + child.GUIComponent.UpdateManually(deltaTime, recursive, recursive); + } } #endregion @@ -583,7 +592,10 @@ namespace Barotrauma /// public virtual void DrawChildren(SpriteBatch spriteBatch, bool recursive) { - RectTransform.Children.ForEach(c => c.GUIComponent.DrawManually(spriteBatch, recursive, recursive)); + foreach (RectTransform child in RectTransform.Children) + { + child.GUIComponent.DrawManually(spriteBatch, recursive, recursive); + } } protected Color _currentColor; @@ -764,8 +776,8 @@ namespace Barotrauma { 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)); + (int)(toolTipBlock.Font.MeasureString(toolTipBlock.WrappedText).X + padding.X + toolTipBlock.Padding.X + toolTipBlock.Padding.Z), + (int)(toolTipBlock.Font.MeasureString(toolTipBlock.WrappedText).Y + padding.Y + toolTipBlock.Padding.Y + toolTipBlock.Padding.W)); toolTipBlock.userData = toolTip; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs index 7d932bcc9..ba0faa08c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs @@ -827,6 +827,13 @@ namespace Barotrauma protected override void Update(float deltaTime) { + foreach (GUIComponent child in Children) + { + if (child == ScrollBar || child == Content || child == ContentBackground) { continue; } + + throw new InvalidOperationException($"Children were found in {nameof(GUIListBox)}, Add them to {nameof(GUIListBox)}.{nameof(Content)} instead."); + } + if (!Visible) { return; } UpdateChildrenRect(); @@ -837,7 +844,6 @@ namespace Barotrauma UpdateScrollBarSize(); } - if (FadeElements) { foreach (var (component, _) in childVisible) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs index 232c13c6a..62ebe85bb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs @@ -319,28 +319,29 @@ namespace Barotrauma AbsoluteSpacing = absoluteSpacing.Y, }; - var bottomContainer = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.3f), verticalLayoutGroup.RectTransform), style: null); - - var tickBoxLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.67f, 1.0f), bottomContainer.RectTransform, anchor: Anchor.CenterLeft), - isHorizontal: true, childAnchor: Anchor.CenterLeft) + var bottomContainer = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.3f), verticalLayoutGroup.RectTransform), style: null) { - Stretch = true, - RelativeSpacing = 0.02f + CanBeFocused = true }; - var dontShowAgainTickBox = new GUITickBox(new RectTransform(new Vector2(0.5f, 1.0f), tickBoxLayoutGroup.RectTransform), + var tickBoxLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.67f, 1.0f), bottomContainer.RectTransform, anchor: Anchor.CenterLeft)) + { + CanBeFocused = true, + Stretch = true + }; + Vector2 tickBoxRelativeSize = new Vector2(1.0f, 0.5f); + var dontShowAgainTickBox = new GUITickBox(new RectTransform(tickBoxRelativeSize, tickBoxLayoutGroup.RectTransform), TextManager.Get("hintmessagebox.dontshowagain")) { ToolTip = TextManager.Get("hintmessagebox.dontshowagaintooltip"), UserData = "dontshowagain" }; - - //var disableHintsTickBox = new GUITickBox(new RectTransform(new Vector2(0.33f, 1.0f), tickBoxLayoutGroup.RectTransform), - // TextManager.Get("hintmessagebox.disablehints")) - //{ - // ToolTip = TextManager.Get("hintmessagebox.disablehintstooltip"), - // UserData = "disablehints" - //}; + var disableHintsTickBox = new GUITickBox(new RectTransform(tickBoxRelativeSize, tickBoxLayoutGroup.RectTransform), + TextManager.Get("hintmessagebox.disablehints")) + { + ToolTip = TextManager.Get("hintmessagebox.disablehintstooltip"), + UserData = "disablehints" + }; Buttons = new List(1) { @@ -379,12 +380,16 @@ namespace Barotrauma upperContainerHeight = Math.Max(upperContainerHeight, Icon.Rect.Height); height += upperContainerHeight; height += absoluteSpacing.Y; - height += (int)((bottomContainer.RectTransform.RelativeSize.Y / topHorizontalLayoutGroup.RectTransform.RelativeSize.Y) * upperContainerHeight); + int bottomContainerHeight = dontShowAgainTickBox.Rect.Height + disableHintsTickBox.Rect.Height; + height += bottomContainerHeight; height += absoluteSpacing.Y; if (minSize.HasValue) { height = Math.Max(height, minSize.Value.Y); } InnerFrame.RectTransform.NonScaledSize = new Point(InnerFrame.Rect.Width, height); verticalLayoutGroup.RectTransform.NonScaledSize = GetVerticalLayoutGroupSize(); + float upperContainerRelativeHeight = (float)upperContainerHeight / (upperContainerHeight + bottomContainerHeight); + topHorizontalLayoutGroup.RectTransform.RelativeSize = new Vector2(topHorizontalLayoutGroup.RectTransform.RelativeSize.X, upperContainerRelativeHeight); + bottomContainer.RectTransform.RelativeSize = new Vector2(bottomContainer.RectTransform.RelativeSize.X, 1.0f - upperContainerRelativeHeight); verticalLayoutGroup.Recalculate(); topHorizontalLayoutGroup.Recalculate(); Content.Recalculate(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScissorComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScissorComponent.cs index 07119e38a..3969a3db3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScissorComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIScissorComponent.cs @@ -29,6 +29,12 @@ namespace Barotrauma ClampChildMouseRects(Content); } + public override void DrawChildren(SpriteBatch spriteBatch, bool recursive) + { + //do nothing (the children have to be drawn in the Draw method after the ScissorRectangle has been set) + return; + } + protected override void Draw(SpriteBatch spriteBatch) { if (!Visible) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs index 6dc1d5f03..031ec550b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs @@ -204,6 +204,12 @@ namespace Barotrauma set { textColor = value; } } + public Color DisabledTextColor + { + get => disabledTextColor; + set => disabledTextColor = value; + } + private Color? hoverTextColor; public Color HoverTextColor { @@ -303,6 +309,10 @@ namespace Barotrauma if (parseRichText) { RichTextData = Barotrauma.RichTextData.GetRichTextData(text, out text); + if (RichTextData != null && RichTextData.Count == 0) + { + RichTextData = null; + } } //if the text is in chinese/korean/japanese and we're not using a CJK-compatible font, @@ -457,7 +467,7 @@ namespace Barotrauma while (size == Vector2.Zero) { try { size = Font.MeasureString(string.IsNullOrEmpty(text) ? " " : text); } - catch { text = text.Substring(0, text.Length - 1); } + catch { text = text.Length > 0 ? text.Substring(0, text.Length - 1) : ""; } } return size; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index d09434b5d..147a24e37 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -70,6 +70,8 @@ namespace Barotrauma private Vector2 selectionEndPos; private Vector2 selectionRectSize; + private GUICustomComponent caretAndSelectionRenderer; + private bool mouseHeldInside; private readonly Memento memento = new Memento(); @@ -178,8 +180,7 @@ namespace Barotrauma } set { - base.ToolTip = value; - textBlock.ToolTip = value; + base.ToolTip = textBlock.ToolTip = caretAndSelectionRenderer.ToolTip = value; } } @@ -268,7 +269,7 @@ namespace Barotrauma CaretEnabled = true; caretPosDirty = true; - new GUICustomComponent(new RectTransform(Vector2.One, frame.RectTransform), onDraw: DrawCaretAndSelection); + caretAndSelectionRenderer = new GUICustomComponent(new RectTransform(Vector2.One, frame.RectTransform), onDraw: DrawCaretAndSelection); int clearButtonWidth = 0; if (createClearButton) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs new file mode 100644 index 000000000..dc6321e84 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs @@ -0,0 +1,1056 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + [SuppressMessage("ReSharper", "UnusedVariable")] + internal class MedicalClinicUI + { + private enum ElementState + { + Enabled, + Disabled + } + + // Represents a pending affliction in the right side pending heal list + private struct PendingAfflictionElement + { + public readonly GUIComponent UIElement; + public readonly MedicalClinic.NetAffliction Target; + public readonly GUITextBlock Price; + + public PendingAfflictionElement(MedicalClinic.NetAffliction target, GUIComponent element, GUITextBlock price) + { + UIElement = element; + Target = target; + Price = price; + } + } + + // Represents a pending heal on the right side list + private struct PendingHealElement + { + public readonly GUIComponent UIElement; + public MedicalClinic.NetCrewMember Target; + public readonly GUIListBox AfflictionList; + public readonly List Afflictions; + + public PendingHealElement(MedicalClinic.NetCrewMember target, GUIComponent element, GUIListBox afflictionList) + { + UIElement = element; + Target = target; + AfflictionList = afflictionList; + Afflictions = new List(); + } + + public PendingAfflictionElement? FindAfflictionElement(MedicalClinic.NetAffliction target) => Afflictions.FirstOrNull(element => element.Target.Identifier.Equals(target.Identifier, StringComparison.OrdinalIgnoreCase)); + } + + // Represents an affliction on the left side crew entry + private readonly struct AfflictionElement + { + public readonly GUIImage? UIImage; + public readonly GUIComponent UIElement; + public readonly MedicalClinic.NetAffliction Target; + + public AfflictionElement(MedicalClinic.NetAffliction target, GUIComponent element, GUIImage? icon) + { + UIElement = element; + UIImage = icon; + Target = target; + } + } + + // Represent an entry on the left side crew list + private readonly struct CrewElement + { + public readonly GUIComponent UIElement; + public readonly CharacterInfo Target; + public readonly GUIListBox AfflictionList; + public readonly List Afflictions; + public readonly GUIComponent OverflowIndicator; + + public CrewElement(CharacterInfo target, GUIComponent overflowIndicator, GUIComponent element, GUIListBox afflictionList) + { + OverflowIndicator = overflowIndicator; + UIElement = element; + Target = target; + AfflictionList = afflictionList; + Afflictions = new List(); + } + } + + // Represents the right side pending list + private readonly struct PendingHealList + { + public readonly GUIListBox HealList; + public readonly GUITextBlock? ErrorBlock; + public readonly GUITextBlock PriceBlock; + public readonly List HealElements; + public readonly GUIButton HealButton; + + public PendingHealList(GUIListBox healList, GUITextBlock priceBlock, GUIButton healButton, GUITextBlock? errorBlock) + { + HealList = healList; + ErrorBlock = errorBlock; + PriceBlock = priceBlock; + HealButton = healButton; + HealElements = new List(); + } + + public void UpdateElement(PendingHealElement newElement) + { + foreach (PendingHealElement element in HealElements.ToList()) + { + if (element.Target.CharacterEquals(newElement.Target)) + { + HealElements.Remove(element); + HealElements.Add(newElement); + return; + } + } + } + + public PendingHealElement? FindCrewElement(MedicalClinic.NetCrewMember crewMember) => HealElements.FirstOrNull(element => element.Target.CharacterInfoID == crewMember.CharacterInfoID); + } + + // Represents the left side crew list + private readonly struct CrewHealList + { + public readonly GUIComponent Panel; + public readonly GUIListBox HealList; + public readonly List HealElements; + + public CrewHealList(GUIListBox healList, GUIComponent panel) + { + Panel = panel; + HealList = healList; + HealElements = new List(); + } + } + + private readonly struct PopupAffliction + { + public readonly MedicalClinic.NetAffliction Target; + public readonly ImmutableArray ElementsToDisable; + + public PopupAffliction(ImmutableArray elementsToDisable, MedicalClinic.NetAffliction target) + { + Target = target; + ElementsToDisable = elementsToDisable; + } + } + + private readonly struct PopupAfflictionList + { + public readonly MedicalClinic.NetCrewMember Target; + public readonly GUIButton TreatAllButton; + public readonly List Afflictions; + + public PopupAfflictionList(MedicalClinic.NetCrewMember crewMember, GUIButton treatAllButton) + { + Target = crewMember; + TreatAllButton = treatAllButton; + Afflictions = new List(); + } + } + + // private enum SortMode + // { + // Severity + // } + + private readonly MedicalClinic medicalClinic; + private readonly GUIComponent container; + private Point prevResolution; + + private PendingHealList? pendingHealList; + private CrewHealList? crewHealList; + + private GUIFrame? selectedCrewElement; + private PopupAfflictionList? selectedCrewAfflictionList; + private bool isWaitingForServer; + private const float refreshTimerMax = 3f; + private float refreshTimer = 0; + + public MedicalClinicUI(MedicalClinic clinic, GUIComponent parent) + { + medicalClinic = clinic; + container = parent; + clinic.OnUpdate = OnMedicalClinicUpdated; + +#if DEBUG + // creates a button that re-creates the UI + CreateRefreshButton(); + void CreateRefreshButton() + { + new GUIButton(new RectTransform(new Vector2(0.2f, 0.1f), parent.RectTransform, Anchor.TopCenter), "Recreate UI - NOT PRESENT IN RELEASE!") + { + OnClicked = (_, __) => + { + parent.ClearChildren(); + CreateUI(); + CreateRefreshButton(); + RequestLatestPending(); + return true; + } + }; + } +#endif + CreateUI(); + } + + private void OnMedicalClinicUpdated() + { + UpdateCrewPanel(); + UpdatePending(); + UpdatePopupAfflictions(); + } + + private void UpdatePopupAfflictions() + { + if (selectedCrewAfflictionList is { } afflictionList) + { + foreach (PopupAffliction popupAffliction in afflictionList.Afflictions) + { + ToggleElements(ElementState.Enabled, popupAffliction.ElementsToDisable); + if (medicalClinic.IsAfflictionPending(afflictionList.Target, popupAffliction.Target)) + { + ToggleElements(ElementState.Disabled, popupAffliction.ElementsToDisable); + } + } + + afflictionList.TreatAllButton.Enabled = true; + if (afflictionList.Afflictions.All(a => medicalClinic.IsAfflictionPending(afflictionList.Target, a.Target))) + { + afflictionList.TreatAllButton.Enabled = false; + } + } + } + + private void UpdatePending() + { + if (!(pendingHealList is { } healList)) { return; } + + ImmutableArray pendingList = medicalClinic.PendingHeals.ToImmutableArray(); + + // check if there are crew members that are not in the UI + foreach (MedicalClinic.NetCrewMember crewMember in pendingList) + { + if (healList.FindCrewElement(crewMember) is { } element) + { + element.Target = crewMember; + healList.UpdateElement(element); + continue; + } + + CreatePendingHealElement(healList.HealList.Content, crewMember, healList, Array.Empty()); + } + + // check if there are elements that the crew doesn't have + foreach (PendingHealElement element in healList.HealElements.ToList()) + { + if (pendingList.Any(member => member.CharacterEquals(element.Target))) + { + UpdatePendingAfflictions(element); + continue; + } + + healList.HealElements.Remove(element); + healList.HealList.Content.RemoveChild(element.UIElement); + } + + int totalCost = medicalClinic.GetTotalCost(); + healList.PriceBlock.Text = UpgradeStore.FormatCurrency(totalCost); + healList.PriceBlock.TextColor = GUI.Style.Red; + healList.HealButton.Enabled = false; + if (medicalClinic.GetMoney() > totalCost) + { + healList.PriceBlock.TextColor = GUI.Style.TextColor; + if (medicalClinic.PendingHeals.Any()) + { + healList.HealButton.Enabled = true; + } + } + } + + private void UpdatePendingAfflictions(PendingHealElement element) + { + MedicalClinic.NetCrewMember crewMember = element.Target; + foreach (MedicalClinic.NetAffliction affliction in crewMember.Afflictions.ToList()) + { + if (element.FindAfflictionElement(affliction) is { } existingAffliction) + { + existingAffliction.Price.Text = UpgradeStore.FormatCurrency(affliction.Strength); + continue; + } + + CreatePendingAffliction(element.AfflictionList, crewMember, affliction, element); + } + + foreach (PendingAfflictionElement afflictionElement in element.Afflictions.ToList()) + { + if (crewMember.Afflictions.Any(affliction => affliction.AfflictionEquals(afflictionElement.Target))) { continue; } + + element.Afflictions.Remove(afflictionElement); + element.AfflictionList.Content.RemoveChild(afflictionElement.UIElement); + } + } + + private void UpdateCrewPanel() + { + if (!(crewHealList is { } healList)) { return; } + + ImmutableArray crew = MedicalClinic.GetCrewCharacters(); + + // check if there are crew members that are not in the UI + foreach (CharacterInfo info in crew) + { + if (healList.HealElements.Any(element => element.Target == info)) { continue; } + + CreateCrewEntry(healList.HealList.Content, healList, info, healList.Panel); + } + + // check if there are elements that the crew doesn't have + foreach (CrewElement element in healList.HealElements.ToList()) + { + if (crew.Any(info => element.Target == info)) + { + UpdateAfflictionList(element); + continue; + } + + healList.HealElements.Remove(element); + healList.HealList.Content.RemoveChild(element.UIElement); + } + + IEnumerable orderedList = healList.HealElements.OrderBy(element => element.Target.Character?.HealthPercentage ?? 100); + + foreach (CrewElement element in orderedList) + { + element.UIElement.SetAsLastChild(); + } + } + + private static void UpdateAfflictionList(CrewElement healElement) + { + CharacterHealth? health = healElement.Target.Character?.CharacterHealth; + if (health is null) { return; } + + // sum up all the afflictions and their strengths + Dictionary afflictionAndStrength = new Dictionary(); + + foreach (Affliction affliction in health.GetAllAfflictions().Where(a => !a.Prefab.IsBuff && a.Strength > 0)) + { + if (afflictionAndStrength.TryGetValue(affliction.Prefab, out float strength)) + { + strength += affliction.Strength; + afflictionAndStrength[affliction.Prefab] = strength; + continue; + } + + afflictionAndStrength.Add(affliction.Prefab, affliction.Strength); + } + + // hide all the elements because we only want to show 3 later on + foreach (AfflictionElement element in healElement.Afflictions) + { + element.UIElement.Visible = false; + } + + healElement.OverflowIndicator.Visible = false; + + foreach (var (prefab, strength) in afflictionAndStrength) + { + bool found = false; + foreach (AfflictionElement existingElement in healElement.Afflictions) + { + if (!existingElement.Target.AfflictionEquals(prefab)) { continue; } + + if (existingElement.UIImage is { } icon) + { + icon.Color = CharacterHealth.GetAfflictionIconColor(prefab, strength); + } + + found = true; + } + + if (found) { continue; } + + CreateCrewAfflictionIcon(healElement, healElement.AfflictionList.Content, prefab, strength); + } + + foreach (AfflictionElement element in healElement.Afflictions.ToList()) + { + if (afflictionAndStrength.Any(pair => element.Target.AfflictionEquals(pair.Key))) { continue; } + + healElement.AfflictionList.Content.RemoveChild(element.UIElement); + healElement.Afflictions.Remove(element); + } + + for (int i = 0; i < 3 && i < healElement.Afflictions.Count; i++) + { + healElement.Afflictions[i].UIElement.Visible = true; + } + + healElement.OverflowIndicator.Visible = healElement.Afflictions.Count > 3; + healElement.OverflowIndicator.SetAsLastChild(); + } + + private static void CreateCrewAfflictionIcon(CrewElement healElement, GUIComponent parent, AfflictionPrefab prefab, float strength) + { + GUIFrame backgroundFrame = new GUIFrame(new RectTransform(new Vector2(0.25f, 1f), parent.RectTransform), style: null) + { + CanBeFocused = false, + Visible = false + }; + + GUIImage? uiIcon = null; + if (prefab.Icon is { } icon) + { + uiIcon = new GUIImage(new RectTransform(Vector2.One, backgroundFrame.RectTransform), icon, scaleToFit: true) + { + Color = CharacterHealth.GetAfflictionIconColor(prefab, strength) + }; + } + + healElement.Afflictions.Add(new AfflictionElement(new MedicalClinic.NetAffliction { Prefab = prefab }, backgroundFrame, uiIcon)); + } + + private void CreateUI() + { + container.ClearChildren(); + pendingHealList = null; + int panelMaxWidth = (int)(GUI.xScale * (GUI.HorizontalAspectRatio < 1.4f ? 650 : 560)); + + GUIFrame paddedParent = new GUIFrame(new RectTransform(new Vector2(0.95f), container.RectTransform, Anchor.Center), style: null); + + GUILayoutGroup clinicContent = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 1.0f), paddedParent.RectTransform) + { + MaxSize = new Point(panelMaxWidth, container.Rect.Height) + }) + { + Stretch = true, + RelativeSpacing = 0.01f + }; + + GUILayoutGroup clinicLabelLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), clinicContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + GUIImage clinicIcon = new GUIImage(new RectTransform(Vector2.One, clinicLabelLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "CrewManagementHeaderIcon", scaleToFit: true); + GUITextBlock clinicLabel = new GUITextBlock(new RectTransform(Vector2.One, clinicLabelLayout.RectTransform), TextManager.Get("medicalclinic.medicalclinic"), font: GUI.LargeFont); + + GUIFrame clinicBackground = new GUIFrame(new RectTransform(Vector2.One, clinicContent.RectTransform)); + + CreateLeftSidePanel(clinicBackground); + + GUILayoutGroup crewContent = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 1.0f), paddedParent.RectTransform, anchor: Anchor.TopRight) + { + MaxSize = new Point(panelMaxWidth, container.Rect.Height) + }) + { + Stretch = true, + RelativeSpacing = 0.01f + }; + + GUILayoutGroup balanceLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), crewContent.RectTransform)); + GUITextBlock balanceLabel = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), balanceLayout.RectTransform), TextManager.Get("campaignstore.balance"), textAlignment: Alignment.BottomRight, font: GUI.Font) + { + AutoScaleVertical = true, + ForceUpperCase = true + }; + + GUITextBlock moneyLabel = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), balanceLayout.RectTransform), string.Empty, textAlignment: Alignment.TopRight, font: GUI.Style.SubHeadingFont) + { + TextGetter = () => UpgradeStore.FormatCurrency(medicalClinic.GetMoney()), + AutoScaleVertical = true, + TextScale = 1.1f + }; + + GUIFrame crewBackground = new GUIFrame(new RectTransform(Vector2.One, crewContent.RectTransform)); + + CreateRightSidePanel(crewBackground); + + prevResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); + } + + private void CreateLeftSidePanel(GUIComponent parent) + { + crewHealList = null; + GUILayoutGroup clinicContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), parent.RectTransform, Anchor.Center)) + { + RelativeSpacing = 0.015f, + Stretch = true + }; + + // GUILayoutGroup sortLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.05f), clinicContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + + // new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), sortLayout.RectTransform), TextManager.Get("campaignstore.sortby"), font: GUI.SubHeadingFont); + + // GUIDropDown sortDropdown = new GUIDropDown(new RectTransform(new Vector2(0.3f, 1f), sortLayout.RectTransform)); + // + // foreach (SortMode mode in Enum.GetValues(typeof(SortMode)).Cast()) + // { + // sortDropdown.AddItem(TextManager.Get($"medicalclinic.sortmode.{mode}"), mode); + // } + // + // sortDropdown.SelectItem(SortMode.Severity); + + GUIListBox crewList = new GUIListBox(new RectTransform(Vector2.One, clinicContainer.RectTransform)); + + crewHealList = new CrewHealList(crewList, parent); + } + + private void CreateCrewEntry(GUIComponent parent, CrewHealList healList, CharacterInfo info, GUIComponent panel) + { + GUIButton crewBackground = new GUIButton(new RectTransform(new Vector2(1f, 0.1f), parent.RectTransform), style: "ListBoxElement"); + + GUILayoutGroup crewLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f), crewBackground.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft); + + GUILayoutGroup characterBlockLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 0.9f), crewLayout.RectTransform), isHorizontal: true, Anchor.CenterLeft); + CreateCharacterBlock(characterBlockLayout, info); + + GUIListBox afflictionList = new GUIListBox(new RectTransform(new Vector2(0.45f, 1f), crewLayout.RectTransform), style: null, isHorizontal: true); + + GUILayoutGroup healthLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.1f, 1f), crewLayout.RectTransform), isHorizontal: true, Anchor.Center); + + new GUITextBlock(new RectTransform(Vector2.One, healthLayout.RectTransform), string.Empty, textAlignment: Alignment.Center, font: GUI.SubHeadingFont) + { + TextGetter = () => $"{(int)(info.Character?.HealthPercentage ?? 100f)}%", + TextColor = GUI.Style.Green + }; + + GUITextBlock overflowIndicator = + new GUITextBlock(new RectTransform(new Vector2(0.25f, 1f), afflictionList.Content.RectTransform, scaleBasis: ScaleBasis.BothHeight), text: "+", textAlignment: Alignment.Center, font: GUI.LargeFont) + { + Visible = false, + CanBeFocused = false, + TextColor = GUI.Style.Red + }; + + MedicalClinic.NetCrewMember member = new MedicalClinic.NetCrewMember { CharacterInfo = info, Afflictions = Array.Empty() }; + + crewBackground.OnClicked = (_, __) => + { + SelectCharacter(member, new Vector2(panel.Rect.Right, crewBackground.Rect.Top)); + return true; + }; + + healList.HealElements.Add(new CrewElement(info, overflowIndicator, crewBackground, afflictionList)); + } + + private void CreateRightSidePanel(GUIComponent parent) + { + GUILayoutGroup pendingHealContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), parent.RectTransform, anchor: Anchor.Center)) + { + RelativeSpacing = 0.015f, + Stretch = true + }; + + new GUITextBlock(new RectTransform(new Vector2(1f, 0.05f), pendingHealContainer.RectTransform), TextManager.Get("medicalclinic.pendingheals"), font: GUI.SubHeadingFont); + + GUIFrame healListContainer = new GUIFrame(new RectTransform(new Vector2(1f, 0.9f), pendingHealContainer.RectTransform), style: null); + GUITextBlock? errorBlock = null; + if (!GameMain.IsSingleplayer) + { + errorBlock = new GUITextBlock(new RectTransform(Vector2.One, healListContainer.RectTransform), text: TextManager.Get("pleasewaitupnp"), font: GUI.LargeFont, textAlignment: Alignment.Center); + } + + GUIListBox healList = new GUIListBox(new RectTransform(Vector2.One, healListContainer.RectTransform)) + { + Spacing = GUI.IntScale(8), + Visible = GameMain.IsSingleplayer + }; + + GUILayoutGroup footerLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), pendingHealContainer.RectTransform)); + + GUILayoutGroup priceLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), footerLayout.RectTransform), isHorizontal: true); + GUITextBlock priceLabelBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), priceLayout.RectTransform), TextManager.Get("campaignstore.total")); + GUITextBlock priceBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), priceLayout.RectTransform), UpgradeStore.FormatCurrency(medicalClinic.GetTotalCost()), font: GUI.SubHeadingFont, + textAlignment: Alignment.Right); + + GUILayoutGroup buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), footerLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterRight); + GUIButton healButton = new GUIButton(new RectTransform(new Vector2(0.33f, 1f), buttonLayout.RectTransform), TextManager.Get("medicalclinic.heal")) + { + Enabled = medicalClinic.PendingHeals.Any() && medicalClinic.GetTotalCost() < medicalClinic.GetMoney(), + OnClicked = (button, _) => + { + button.Enabled = false; + ClosePopup(); + medicalClinic.HealAllButtonAction(request => + { + switch (request.HealResult) + { + case MedicalClinic.HealRequestResult.InsufficientFunds: + GUI.NotifyPrompt(TextManager.Get("medicalclinic.unabletoheal"), TextManager.Get("medicalclinic.insufficientfunds")); + break; + case MedicalClinic.HealRequestResult.Refused: + GUI.NotifyPrompt(TextManager.Get("medicalclinic.unabletoheal"), TextManager.Get("medicalclinic.healrefused")); + break; + } + + button.Enabled = true; + }); + return true; + } + }; + + GUIButton clearButton = new GUIButton(new RectTransform(new Vector2(0.33f, 1f), buttonLayout.RectTransform), TextManager.Get("campaignstore.clearall")) + { + OnClicked = (button, _) => + { + button.Enabled = false; + medicalClinic.ClearAllButtonAction(_ => + { + button.Enabled = true; + }); + return true; + } + }; + + PendingHealList list = new PendingHealList(healList, priceBlock, healButton, errorBlock); + + foreach (MedicalClinic.NetCrewMember heal in GetPendingCharacters()) + { + CreatePendingHealElement(healList.Content, heal, list, heal.Afflictions); + } + + pendingHealList = list; + } + + private void CreatePendingHealElement(GUIComponent parent, MedicalClinic.NetCrewMember crewMember, PendingHealList healList, MedicalClinic.NetAffliction[] afflictions) + { + CharacterInfo? healInfo = crewMember.FindCharacterInfo(MedicalClinic.GetCrewCharacters()); + if (healInfo is null) { return; } + + GUIFrame pendingHealBackground = new GUIFrame(new RectTransform(new Vector2(1f, 0.25f), parent.RectTransform)); + GUILayoutGroup pendingHealLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f), pendingHealBackground.RectTransform, Anchor.Center)); + + GUILayoutGroup topHeaderLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.3f), pendingHealLayout.RectTransform), isHorizontal: true, Anchor.CenterLeft) { Stretch = true }; + + CreateCharacterBlock(topHeaderLayout, healInfo); + + GUILayoutGroup bottomLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.7f), pendingHealLayout.RectTransform), childAnchor: Anchor.Center); + + GUIListBox pendingAfflictionList = new GUIListBox(new RectTransform(Vector2.One, bottomLayout.RectTransform)) + { + AutoHideScrollBar = false, + ScrollBarVisible = true + }; + + PendingHealElement healElement = new PendingHealElement(crewMember, pendingHealBackground, pendingAfflictionList); + + foreach (MedicalClinic.NetAffliction affliction in afflictions) + { + CreatePendingAffliction(pendingAfflictionList, crewMember, affliction, healElement); + } + + healList.HealElements.Add(healElement); + RecalculateLayouts(pendingHealLayout, topHeaderLayout, bottomLayout); + pendingAfflictionList.ForceUpdate(); + } + + private void CreatePendingAffliction(GUIListBox parent, MedicalClinic.NetCrewMember crewMember, MedicalClinic.NetAffliction affliction, PendingHealElement healElement) + { + GUIFrame backgroundFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.33f), parent.Content.RectTransform), style: "ListBoxElement"); + GUILayoutGroup parentLayout = new GUILayoutGroup(new RectTransform(Vector2.One, backgroundFrame.RectTransform), isHorizontal: true) { Stretch = true }; + + if (!(affliction.Prefab is { } prefab)) { return; } + + if (prefab.Icon is { } icon) + { + new GUIImage(new RectTransform(Vector2.One, parentLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), icon, scaleToFit: true) + { + Color = CharacterHealth.GetAfflictionIconColor(prefab, affliction.Strength) + }; + } + + GUILayoutGroup textLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parentLayout.RectTransform), isHorizontal: true); + + string name = prefab.Name; + + GUIFrame textContainer = new GUIFrame(new RectTransform(new Vector2(0.6f, 1f), textLayout.RectTransform), style: null); + GUITextBlock afflictionName = new GUITextBlock(new RectTransform(Vector2.One, textContainer.RectTransform), name, font: GUI.SubHeadingFont); + + GUITextBlock healCost = new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), textLayout.RectTransform), UpgradeStore.FormatCurrency(affliction.Price), textAlignment: Alignment.Center, font: GUI.LargeFont) + { + Padding = Vector4.Zero + }; + + GUIButton healButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), textLayout.RectTransform), style: "CrewManagementRemoveButton") + { + OnClicked = (button, _) => + { + button.Enabled = false; + medicalClinic.RemovePendingButtonAction(crewMember, affliction, _ => + { + button.Enabled = true; + }); + return true; + } + }; + + EnsureTextDoesntOverflow(name, afflictionName, textContainer.Rect, ImmutableArray.Create(textLayout, parentLayout)); + + healElement.Afflictions.Add(new PendingAfflictionElement(affliction, backgroundFrame, healCost)); + + RecalculateLayouts(parentLayout, textLayout); + + parent.ForceUpdate(); + } + + private static void CreateCharacterBlock(GUIComponent parent, CharacterInfo info) + { + new GUICustomComponent(new RectTransform(Vector2.One, parent.RectTransform, scaleBasis: ScaleBasis.BothHeight), (spriteBatch, component) => + { + info.DrawPortrait(spriteBatch, component.Rect.Location.ToVector2(), Vector2.Zero, component.Rect.Width); + }); + + GUILayoutGroup textGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.8f), parent.RectTransform)); + + string? characterName = info.Name, + jobName = null; + + GUITextBlock? nameBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), textGroup.RectTransform), characterName), + jobBlock = null; + + if (info.Job is { Name: { } name, Prefab: { UIColor: var color} } job) + { + jobName = name; + jobBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), textGroup.RectTransform), jobName); + nameBlock.TextColor = color; + } + + if (parent is GUILayoutGroup layoutGroup) + { + ImmutableArray layoutGroups = ImmutableArray.Create(layoutGroup, textGroup); + + EnsureTextDoesntOverflow(characterName, nameBlock, parent.Rect, layoutGroups); + + if (jobBlock is null) { return; } + + EnsureTextDoesntOverflow(jobName, jobBlock, parent.Rect, layoutGroups); + } + } + + private void SelectCharacter(MedicalClinic.NetCrewMember crewMember, Vector2 location) + { + CharacterInfo? info = crewMember.FindCharacterInfo(MedicalClinic.GetCrewCharacters()); + if (info is null) { return; } + + if (isWaitingForServer) { return; } + + ClosePopup(); + + GUIFrame mainFrame = new GUIFrame(new RectTransform(new Vector2(0.28f, 0.45f), container.RectTransform) + { + ScreenSpaceOffset = location.ToPoint() + }); + + GUILayoutGroup mainLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f), mainFrame.RectTransform, Anchor.Center)) { RelativeSpacing = 0.01f, Stretch = true }; + + if (mainFrame.Rect.Bottom > GameMain.GraphicsHeight) + { + mainFrame.RectTransform.ScreenSpaceOffset = new Point((int)location.X, GameMain.GraphicsHeight - mainFrame.Rect.Height); + } + + GUITextBlock feedbackBlock = new GUITextBlock(new RectTransform(Vector2.One, mainFrame.RectTransform), TextManager.Get("pleasewaitupnp"), textAlignment: Alignment.Center, font: GUI.LargeFont) + { + Visible = true + }; + + GUIButton treatAllButton = new GUIButton(new RectTransform(new Vector2(1f, 0.2f), mainLayout.RectTransform), TextManager.Get("medicalclinic.treatall")) + { + Font = GUI.SubHeadingFont, + Visible = false + }; + + GUIListBox afflictionList = new GUIListBox(new RectTransform(new Vector2(1f, 0.8f), mainLayout.RectTransform)) { Visible = false }; + + PopupAfflictionList popupAfflictionList = new PopupAfflictionList(crewMember, treatAllButton); + selectedCrewElement = mainFrame; + selectedCrewAfflictionList = popupAfflictionList; + + isWaitingForServer = true; + medicalClinic.RequestAfflictions(info, OnReceived); + + void OnReceived(MedicalClinic.AfflictionRequest request) + { + isWaitingForServer = false; + + if (request.Result != MedicalClinic.RequestResult.Success) + { + feedbackBlock.Text = GetErrorText(request.Result); + feedbackBlock.TextColor = GUI.Style.Red; + return; + } + + List allComponents = new List(); + foreach (MedicalClinic.NetAffliction affliction in request.Afflictions) + { + ImmutableArray createdComponents = CreatePopupAffliction(afflictionList.Content, crewMember, affliction); + allComponents.AddRange(createdComponents); + popupAfflictionList.Afflictions.Add(new PopupAffliction(createdComponents, affliction)); + } + + allComponents.Add(treatAllButton); + treatAllButton.OnClicked = (_, __) => + { + ImmutableArray afflictions = request.Afflictions.Where(a => !medicalClinic.IsAfflictionPending(crewMember, a)).ToImmutableArray(); + if (!afflictions.Any()) { return true; } + + AddPending(allComponents.ToImmutableArray(), crewMember, afflictions); + return true; + }; + + afflictionList.Visible = true; + feedbackBlock.Visible = false; + treatAllButton.Visible = true; + UpdatePopupAfflictions(); + } + } + + private ImmutableArray CreatePopupAffliction(GUIComponent parent, MedicalClinic.NetCrewMember crewMember, MedicalClinic.NetAffliction affliction) + { + if (!(affliction.Prefab is { } prefab)) { return ImmutableArray.Empty; } + + GUIFrame backgroundFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.33f), parent.RectTransform), style: "ListBoxElement"); + GUILayoutGroup mainLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f), backgroundFrame.RectTransform, Anchor.Center)) + { + RelativeSpacing = 0.05f + }; + + GUILayoutGroup topLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.33f), mainLayout.RectTransform), isHorizontal: true) { Stretch = true }; + + Color iconColor = CharacterHealth.GetAfflictionIconColor(prefab, affliction.Strength); + + GUIImage icon = new GUIImage(new RectTransform(Vector2.One, topLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), prefab.Icon, scaleToFit: true) + { + Color = iconColor, + DisabledColor = iconColor * 0.5f + }; + + GUILayoutGroup topTextLayout = new GUILayoutGroup(new RectTransform(Vector2.One, topLayout.RectTransform), isHorizontal: true); + + GUITextBlock prefabBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), topTextLayout.RectTransform), prefab.Name, font: GUI.SubHeadingFont); + + Color textColor = Color.Lerp(GUI.Style.Orange, GUI.Style.Red, (int)affliction.AfflictionSeverity / 2f); + + string vitalityText = TextManager.GetWithVariable("medicalclinic.vitalitydifference", "[amount]", (-affliction.Strength).ToString()); + GUITextBlock vitalityBlock = new GUITextBlock(new RectTransform(new Vector2(0.25f, 1f), topTextLayout.RectTransform), vitalityText, textAlignment: Alignment.Center) + { + TextColor = textColor, + DisabledTextColor = textColor * 0.5f, + Padding = Vector4.Zero, + AutoScaleHorizontal = true + }; + + string severityText = TextManager.Get($"AfflictionStrength{affliction.AfflictionSeverity}"); + GUITextBlock severityBlock = new GUITextBlock(new RectTransform(new Vector2(0.25f, 1f), topTextLayout.RectTransform), severityText, textAlignment: Alignment.Center, font: GUI.SubHeadingFont) + { + TextColor = textColor, + DisabledTextColor = textColor * 0.5f, + Padding = Vector4.Zero, + AutoScaleHorizontal = true + }; + + EnsureTextDoesntOverflow(prefab.Name, prefabBlock, prefabBlock.Rect, ImmutableArray.Create(mainLayout, topLayout, topTextLayout)); + + GUILayoutGroup bottomLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.66f), mainLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + + GUILayoutGroup bottomTextLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 1f), bottomLayout.RectTransform)); + GUITextBlock descriptionBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), bottomTextLayout.RectTransform), ToolBox.LimitString(prefab.Description, GUI.IntScale(64)), wrap: true) + { + ToolTip = prefab.Description + }; + + GUITextBlock priceBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), bottomTextLayout.RectTransform), UpgradeStore.FormatCurrency(affliction.Price), font: GUI.LargeFont); + + GUIButton buyButton = new GUIButton(new RectTransform(new Vector2(0.2f, 0.75f), bottomLayout.RectTransform), style: "CrewManagementAddButton"); + + ImmutableArray elementsToDisable = ImmutableArray.Create(prefabBlock, backgroundFrame, icon, vitalityBlock, severityBlock, buyButton, descriptionBlock, priceBlock); + + buyButton.OnClicked = (_, __) => + { + if (!buyButton.Enabled) { return false; } + + AddPending(elementsToDisable, crewMember, ImmutableArray.Create(affliction)); + return true; + }; + + return elementsToDisable; + } + + private void AddPending(ImmutableArray elementsToDisable, MedicalClinic.NetCrewMember crewMember, ImmutableArray afflictions) + { + MedicalClinic.NetCrewMember existingMember; + + if (medicalClinic.PendingHeals.FirstOrNull(m => m.CharacterEquals(crewMember)) is { } foundHeal) + { + existingMember = foundHeal; + } + else + { + MedicalClinic.NetCrewMember newMember = new MedicalClinic.NetCrewMember + { + CharacterInfoID = crewMember.CharacterInfoID, + Afflictions = Array.Empty() + }; + + existingMember = newMember; + } + + foreach (MedicalClinic.NetAffliction affliction in afflictions) + { + if (existingMember.Afflictions.FirstOrNull(a => a.AfflictionEquals(affliction)) != null) + { + return; + } + } + + existingMember.Afflictions = existingMember.Afflictions.Concat(afflictions).ToArray(); + ToggleElements(ElementState.Disabled, elementsToDisable); + medicalClinic.AddPendingButtonAction(existingMember, request => + { + if (request.Result == MedicalClinic.RequestResult.Timeout) + { + ToggleElements(ElementState.Enabled, elementsToDisable); + } + }); + } + + private static void EnsureTextDoesntOverflow(string? text, GUITextBlock textBlock, Rectangle bounds, ImmutableArray? layoutGroups = null) + { + if (string.IsNullOrWhiteSpace(text)) { return; } + + string originalText = text; + + UpdateLayoutGroups(); + + while (textBlock.Rect.X + textBlock.TextSize.X + textBlock.Padding.X + textBlock.Padding.W > bounds.Right) + { + if (string.IsNullOrWhiteSpace(text)) { break; } + + text = text[..^1]; + textBlock.Text = text + "..."; + textBlock.ToolTip = originalText; + + UpdateLayoutGroups(); + } + + void UpdateLayoutGroups() + { + if (layoutGroups is null) { return; } + + foreach (GUILayoutGroup layoutGroup in layoutGroups) + { + layoutGroup.Recalculate(); + } + } + } + + public void RequestLatestPending() + { + UpdateCrewPanel(); + + if (GameMain.IsSingleplayer || !(pendingHealList is { ErrorBlock: { } errorBlock, HealList: { } healList })) { return; } + + errorBlock.Visible = true; + errorBlock.TextColor = GUI.Style.TextColor; + errorBlock.Text = TextManager.Get("pleasewaitupnp"); + healList.Visible = false; + + isWaitingForServer = true; + + medicalClinic.RequestLatestPending(OnReceived); + + void OnReceived(MedicalClinic.PendingRequest request) + { + isWaitingForServer = false; + + if (request.Result != MedicalClinic.RequestResult.Success) + { + errorBlock.Text = GetErrorText(request.Result); + errorBlock.TextColor = GUI.Style.Red; + return; + } + + medicalClinic.PendingHeals.Clear(); + foreach (MedicalClinic.NetCrewMember member in request.CrewMembers) + { + medicalClinic.PendingHeals.Add(member); + } + + OnMedicalClinicUpdated(); + + errorBlock.Visible = false; + healList.Visible = true; + } + } + + private void ClosePopup() + { + if (selectedCrewElement is { } popup) + { + popup.Parent?.RemoveChild(selectedCrewElement); + } + + selectedCrewElement = null; + selectedCrewAfflictionList = null; + } + + private static string GetErrorText(MedicalClinic.RequestResult result) + { + return result switch + { + MedicalClinic.RequestResult.Error => TextManager.Get("error"), + MedicalClinic.RequestResult.Timeout => TextManager.Get("medicalclinic.requesttimeout"), + _ => "What the hell did you just do" // this should never happen + }; + } + + private ImmutableArray GetPendingCharacters() => medicalClinic.PendingHeals.ToImmutableArray(); + + private static void ToggleElements(ElementState state, ImmutableArray elements) + { + foreach (GUIComponent component in elements) + { + component.Enabled = state switch + { + ElementState.Enabled => true, + ElementState.Disabled => false, + _ => throw new ArgumentOutOfRangeException(nameof(state), state, null) + }; + } + } + + private static void RecalculateLayouts(params GUILayoutGroup[] layouts) + { + foreach (GUILayoutGroup layout in layouts) + { + layout.Recalculate(); + } + } + + public void Update(float deltaTime) + { + if (prevResolution.X != GameMain.GraphicsWidth || prevResolution.Y != GameMain.GraphicsHeight) + { + CreateUI(); + } + + refreshTimer += deltaTime; + + if (refreshTimer > refreshTimerMax) + { + UpdateCrewPanel(); + refreshTimer = 0; + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs index 04bbaa485..045547d48 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs @@ -637,7 +637,15 @@ namespace Barotrauma public bool IsParentOf(RectTransform rectT, bool recursive = true) { - return children.Contains(rectT) || (recursive && children.Any(c => c.IsParentOf(rectT))); + if (children.Contains(rectT)) { return true; } + if (recursive) + { + foreach (var child in children) + { + if (child.IsParentOf(rectT)) { return true; } + } + } + return false; } public bool IsChildOf(RectTransform rectT, bool recursive = true) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index 848bfc4b2..1c687913c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -414,15 +414,8 @@ namespace Barotrauma } else { - if (GameMain.Client == null) - { - subsToShow.AddRange(SubmarineInfo.SavedSubmarines.Where(s => s.IsCampaignCompatible && !GameMain.GameSession.OwnedSubmarines.Any(os => os.Name == s.Name))); - } - else - { - subsToShow.AddRange(GameMain.NetLobbyScreen.CampaignSubmarines.Where(s => !GameMain.GameSession.OwnedSubmarines.Any(os => os.Name == s.Name))); - } - + subsToShow.AddRange((GameMain.Client is null ? SubmarineInfo.SavedSubmarines : MultiPlayerCampaign.GetCampaignSubs()) + .Where(s => s.IsCampaignCompatible && !GameMain.GameSession.OwnedSubmarines.Any(os => os.Name == s.Name))); subsToShow.Sort((x, y) => x.SubmarineClass.CompareTo(y.SubmarineClass)); } @@ -446,20 +439,11 @@ namespace Barotrauma if (preview == null) { - SubmarineInfo potentialMatch; - - if (GameMain.Client == null) - { - potentialMatch = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.EqualityCheckVal == info.EqualityCheckVal); - } - else - { - potentialMatch = GameMain.NetLobbyScreen.CampaignSubmarines.FirstOrDefault(s => s.EqualityCheckVal == info.EqualityCheckVal); - } + SubmarineInfo potentialMatch = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.EqualityCheckVal == info.EqualityCheckVal); preview = potentialMatch?.PreviewImage; - // Try from savedsubmarines with name comparison as a backup + // Try name comparison as a backup if (preview == null) { potentialMatch = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == info.Name); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index a5554ebf2..22f06e9cc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -1543,30 +1543,12 @@ namespace Barotrauma GUITextBlock.AutoScaleAndNormalize(skillNames); } - private bool HasUnlockedAllTalents(Character controlledCharacter) - { - if (TalentTree.JobTalentTrees.TryGetValue(controlledCharacter.Info.Job.Prefab.Identifier, out TalentTree talentTree)) - { - foreach (TalentSubTree talentSubTree in talentTree.TalentSubTrees) - { - foreach (TalentOption talentOption in talentSubTree.TalentOptionStages) - { - if (talentOption.Talents.None(t => controlledCharacter.HasTalent(t.Identifier))) - { - return false; - } - } - } - } - return true; - } - private void UpdateTalentButtons() { Character controlledCharacter = Character.Controlled; if (controlledCharacter?.Info == null) { return; } - bool unlockedAllTalents = HasUnlockedAllTalents(controlledCharacter); + bool unlockedAllTalents = controlledCharacter.HasUnlockedAllTalents(); if (unlockedAllTalents) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index 1994a8d59..67dcfc6c1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -16,7 +16,6 @@ using Microsoft.Xna.Framework.Input; namespace Barotrauma { - internal class UpgradeStore { public readonly struct CategoryData @@ -1688,7 +1687,7 @@ namespace Barotrauma private bool HasPermission => campaignUI.Campaign.AllowedToManageCampaign(); - private static string FormatCurrency(int money, bool format = true) + public static string FormatCurrency(int money, bool format = true) { return TextManager.GetWithVariable("CurrencyFormat", "[credits]", format ? string.Format(CultureInfo.InvariantCulture, "{0:N0}", money) : money.ToString()); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 711834cf9..15f703b8f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -437,8 +437,8 @@ namespace Barotrauma TaskPool.Add("AutoUpdateWorkshopItemsAsync", SteamManager.AutoUpdateWorkshopItemsAsync(), (task) => { - bool result = ((Task)task).Result; - + if (!task.TryGetResult(out bool result)) { return; } + Config.WaitingForAutoUpdate = false; }); @@ -756,7 +756,10 @@ namespace Barotrauma } #if DEBUG - CancelQuickStart |= PlayerInput.KeyDown(Keys.LeftShift); + if (PlayerInput.KeyHit(Keys.LeftShift)) + { + CancelQuickStart = !CancelQuickStart; + } if (TitleScreen.LoadState >= 100.0f && !TitleScreen.PlayingSplashScreen && (Config.AutomaticQuickStartEnabled || Config.AutomaticCampaignLoadEnabled || Config.TestScreenEnabled) && FirstLoad && !CancelQuickStart) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index 43f30d3b4..8b0534b26 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -718,7 +718,7 @@ namespace Barotrauma /// Sets the character's current order (if it's close enough to receive messages from orderGiver) and /// displays the order in the crew UI /// - public void SetCharacterOrder(Character character, Order order, string option, int priority, Character orderGiver, Hull targetHull = null) + public void SetCharacterOrder(Character character, Order order, string option, int priority, Character orderGiver, Hull targetHull = null, bool isNewOrder = true) { if (order != null && order.TargetAllCharacters) { @@ -768,7 +768,7 @@ namespace Barotrauma if (IsSinglePlayer) { - orderGiver.Speak(order.GetChatMessage("", hull?.DisplayName, givingOrderToSelf: character == orderGiver), ChatMessageType.Order); + orderGiver.Speak(order.GetChatMessage("", hull?.DisplayName, givingOrderToSelf: character == orderGiver, isNewOrder: isNewOrder), ChatMessageType.Order); } else { @@ -784,7 +784,7 @@ namespace Barotrauma if (IsSinglePlayer) { character.SetOrder(order, option, priority, orderGiver, speak: orderGiver != character); - string message = order?.GetChatMessage(character.Name, orderGiver?.CurrentHull?.DisplayName, givingOrderToSelf: character == orderGiver, orderOption: option, priority: priority); + string message = order?.GetChatMessage(character.Name, orderGiver?.CurrentHull?.DisplayName, givingOrderToSelf: character == orderGiver, orderOption: option, isNewOrder: isNewOrder); orderGiver?.Speak(message); } else if (orderGiver != null) @@ -1071,7 +1071,7 @@ namespace Barotrauma var priority = Math.Max(CharacterInfo.HighestManualOrderPriority - orderList.Content.GetChildIndex(orderComponent), 1); if (orderInfo.ManualPriority == priority) { return; } var character = (Character)orderList.UserData; - SetCharacterOrder(character, orderInfo.Order, orderInfo.OrderOption, priority, Character.Controlled); + SetCharacterOrder(character, orderInfo.Order, orderInfo.OrderOption, priority, Character.Controlled, isNewOrder: false); } private string CreateOrderTooltip(Order orderPrefab, string option, Entity targetEntity) @@ -2582,15 +2582,12 @@ namespace Barotrauma { contextualOrders.Remove(pumpOrderInfo); } - if (contextualOrders.None()) + orderIdentifier = "cleanupitems"; + if (contextualOrders.None(info => info.Order.Identifier.Equals(orderIdentifier))) { - orderIdentifier = "cleanupitems"; - if (contextualOrders.None(info => info.Order.Identifier.Equals(orderIdentifier))) + if (AIObjectiveCleanupItems.IsValidTarget(itemContext, Character.Controlled, checkInventory: false) || AIObjectiveCleanupItems.IsValidContainer(itemContext, Character.Controlled)) { - if (AIObjectiveCleanupItems.IsValidTarget(itemContext, Character.Controlled, checkInventory: false) || AIObjectiveCleanupItems.IsValidContainer(itemContext, Character.Controlled)) - { - contextualOrders.Add(new OrderInfo(new Order(Order.GetPrefab(orderIdentifier), itemContext, targetItem: null, Character.Controlled), null)); - } + contextualOrders.Add(new OrderInfo(new Order(Order.GetPrefab(orderIdentifier), itemContext, targetItem: null, Character.Controlled), null)); } } AddIgnoreOrder(itemContext); @@ -3523,16 +3520,6 @@ namespace Barotrauma InitRound(); } - public void EndRound() - { - //remove characterinfos whose characters have been removed or killed - characterInfos.RemoveAll(c => c.Character == null || c.Character.Removed || c.CauseOfDeath != null); - - characters.Clear(); - crewList.ClearChildren(); - GUIContextMenu.CurrentContextMenu = null; - } - public void Reset() { characters.Clear(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index 95d8d567e..32be3708e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -242,6 +242,11 @@ namespace Barotrauma { ReadyCheckButton.RectTransform.ScreenSpaceOffset = endRoundButton.RectTransform.ScreenSpaceOffset; ReadyCheckButton.DrawManually(spriteBatch); + if (ReadyCheck.ReadyCheckCooldown > DateTime.Now) + { + float progress = (ReadyCheck.ReadyCheckCooldown - DateTime.Now).Seconds / 60.0f; + ReadyCheckButton.Color = ToolBox.GradientLerp(progress, Color.White, GUI.Style.Red); + } } } @@ -290,6 +295,9 @@ namespace Barotrauma case InteractionType.Crew when GameMain.NetworkMember != null: CampaignUI.CrewManagement.SendCrewState(false); goto default; + case InteractionType.MedicalClinic: + CampaignUI.MedicalClinic.RequestLatestPending(); + goto default; default: ShowCampaignUI = true; CampaignUI.SelectTab(npc.CampaignInteractionType); @@ -319,6 +327,8 @@ namespace Barotrauma { base.Update(deltaTime); + MedicalClinic?.Update(deltaTime); + if (PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Escape)) { GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData is RoundSummary); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs index 007bef266..113f7e5c3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs @@ -172,7 +172,7 @@ namespace Barotrauma } else { - indicator.Visible = Character.Controlled.Info.GetAvailableTalentPoints() > 0; + indicator.Visible = Character.Controlled.Info.GetAvailableTalentPoints() > 0 && !Character.Controlled.HasUnlockedAllTalents(); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs new file mode 100644 index 000000000..c99349d18 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/MedicalClinic.cs @@ -0,0 +1,386 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Barotrauma.Extensions; +using Barotrauma.Networking; + +namespace Barotrauma +{ + internal partial class MedicalClinic + { + public enum RequestResult + { + Undecided, + Success, + Error, + Timeout + } + + public readonly struct RequestAction + { + public readonly Action Callback; + public readonly DateTimeOffset Timeout; + + public RequestAction(Action callback, DateTimeOffset timeout) + { + Callback = callback; + Timeout = timeout; + } + } + + public readonly struct AfflictionRequest + { + public readonly RequestResult Result; + public readonly ImmutableArray Afflictions; + + public AfflictionRequest(RequestResult result, ImmutableArray afflictions) + { + Result = result; + Afflictions = afflictions; + } + } + + public readonly struct PendingRequest + { + public readonly RequestResult Result; + public readonly ImmutableArray CrewMembers; + + public PendingRequest(RequestResult result, ImmutableArray crewMembers) + { + Result = result; + CrewMembers = crewMembers; + } + } + + public readonly struct CallbackOnlyRequest + { + public readonly RequestResult Result; + + public CallbackOnlyRequest(RequestResult result) + { + Result = result; + } + } + + public readonly struct HealRequest + { + public readonly RequestResult Result; + public readonly HealRequestResult HealResult; + + public HealRequest(RequestResult result, HealRequestResult healResult) + { + Result = result; + HealResult = healResult; + } + } + + private readonly List> afflictionRequests = new List>(); + private readonly List> pendingHealRequests = new List>(); + private readonly List> clearAllRequests = new List>(); + private readonly List> healAllRequests = new List>(); + private readonly List> addRequests = new List>(); + private readonly List> removeRequests = new List>(); + + public void RequestAfflictions(CharacterInfo info, Action onReceived) + { + if (GameMain.IsSingleplayer) + { +#if DEBUG && LINUX + if (Screen.Selected is TestScreen) + { + onReceived.Invoke(new AfflictionRequest(RequestResult.Success, TestAfflictions.ToImmutableArray())); + return; + } +#endif + + if (!(info is { Character: { CharacterHealth: { } health } })) + { + onReceived.Invoke(new AfflictionRequest(RequestResult.Error, ImmutableArray.Empty)); + return; + } + + ImmutableArray pendingAfflictions = GetAllAfflictions(health).ToImmutableArray(); + onReceived.Invoke(new AfflictionRequest(RequestResult.Success, pendingAfflictions)); + return; + } + + afflictionRequests.Add(new RequestAction(onReceived, GetTimeout())); + SendAfflictionRequest(info); + } + + public void RequestLatestPending(Action onReceived) + { + // no need to worry about syncing when there's only one pair of eyes capable of looking at the UI + if (GameMain.IsSingleplayer) { return; } + + pendingHealRequests.Add(new RequestAction(onReceived, GetTimeout())); + SendPendingRequest(); + } + + public void Update(float deltaTime) + { + DateTimeOffset now = DateTimeOffset.Now; + UpdateQueue(afflictionRequests, now, onTimeout: callback => { callback(new AfflictionRequest(RequestResult.Timeout, ImmutableArray.Empty)); }); + UpdateQueue(pendingHealRequests, now, onTimeout: callback => { callback(new PendingRequest(RequestResult.Timeout, ImmutableArray.Empty)); }); + UpdateQueue(healAllRequests, now, onTimeout: callback => { callback(new HealRequest(RequestResult.Timeout, HealRequestResult.Unknown)); }); + UpdateQueue(clearAllRequests, now, onTimeout: CallbackOnlyTimeout); + UpdateQueue(addRequests, now, onTimeout: CallbackOnlyTimeout); + UpdateQueue(removeRequests, now, onTimeout: CallbackOnlyTimeout); + + void CallbackOnlyTimeout(Action callback) { callback(new CallbackOnlyRequest(RequestResult.Timeout)); } + } + + public bool IsAfflictionPending(NetCrewMember character, NetAffliction affliction) + { + foreach (NetCrewMember crewMember in PendingHeals) + { + if (!crewMember.CharacterEquals(character)) { continue; } + + return crewMember.Afflictions.Any(a => a.AfflictionEquals(affliction)); + } + + return false; + } + + private static bool TryDequeue(List> requestQueue, out Action result) + { + RequestAction? first = requestQueue.FirstOrNull(); + if (!(first is { } action)) + { + result = _ => { }; + return false; + } + + requestQueue.Remove(action); + result = action.Callback; + return true; + } + + private static void UpdateQueue(List> requestQueue, DateTimeOffset now, Action> onTimeout) + { + HashSet>? removals = null; + foreach (RequestAction action in requestQueue) + { + if (action.Timeout < now) + { + onTimeout.Invoke(action.Callback); + + removals ??= new HashSet>(); + removals.Add(action); + } + } + + if (removals is null) { return; } + + foreach (RequestAction action in removals) + { + requestQueue.Remove(action); + } + } + + // if you have more than 5000 ping there are probably more important things to worry about but hey just in case + private static DateTimeOffset GetTimeout() => DateTimeOffset.Now.AddSeconds(5).AddMilliseconds(GetPing()); + + private static int GetPing() + { + if (GameMain.IsSingleplayer || !(GameMain.Client?.Name is { } ownName) || !(GameMain.NetworkMember?.ConnectedClients is { } clients)) { return 0; } + + return (from client in clients where client.Name == ownName select client.Ping).FirstOrDefault(); + } + + public void HealAllButtonAction(Action onReceived) + { + if (GameMain.IsSingleplayer) + { + HealRequestResult result = HealAllPending(); + onReceived(new HealRequest(RequestResult.Success, HealAllPending())); + if (result == HealRequestResult.Success) + { + OnUpdate?.Invoke(); + } + + return; + } + + healAllRequests.Add(new RequestAction(onReceived, GetTimeout())); + ClientSend(null, NetworkHeader.HEAL_PENDING, DeliveryMethod.Reliable); + } + + public void ClearAllButtonAction(Action onReceived) + { + if (GameMain.IsSingleplayer) + { + ClearPendingHeals(); + onReceived(new CallbackOnlyRequest(RequestResult.Success)); + OnUpdate?.Invoke(); + return; + } + + clearAllRequests.Add(new RequestAction(onReceived, GetTimeout())); + ClientSend(null, NetworkHeader.CLEAR_PENDING, DeliveryMethod.Reliable); + } + + private void ClearRequstReceived() + { + ClearPendingHeals(); + if (TryDequeue(clearAllRequests, out var callback)) + { + callback(new CallbackOnlyRequest(RequestResult.Success)); + } + OnUpdate?.Invoke(); + } + + private void HealRequestReceived(IReadMessage inc) + { + NetHealRequest request = INetSerializableStruct.Read(inc); + if (request.Result == HealRequestResult.Success) + { + HealAllPending(force: true); + } + + if (TryDequeue(healAllRequests, out var callback)) + { + callback(new HealRequest(RequestResult.Success, request.Result)); + } + + OnUpdate?.Invoke(); + } + + public void AddPendingButtonAction(NetCrewMember crewMember, Action onReceived) + { + if (GameMain.IsSingleplayer) + { + InsertPendingCrewMember(crewMember); + onReceived(new CallbackOnlyRequest(RequestResult.Success)); + OnUpdate?.Invoke(); + return; + } + + addRequests.Add(new RequestAction(onReceived, GetTimeout())); + ClientSend(crewMember, NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable); + } + + public void RemovePendingButtonAction(NetCrewMember crewMember, NetAffliction affliction, Action onReceived) + { + if (GameMain.IsSingleplayer) + { + RemovePendingAffliction(crewMember, affliction); + onReceived(new CallbackOnlyRequest(RequestResult.Success)); + OnUpdate?.Invoke(); + return; + } + + INetSerializableStruct removedAffliction = new NetRemovedAffliction + { + CrewMember = crewMember, + Affliction = affliction + }; + + removeRequests.Add(new RequestAction(onReceived, GetTimeout())); + ClientSend(removedAffliction, NetworkHeader.REMOVE_PENDING, DeliveryMethod.Reliable); + } + + private void NewAdditonReceived(IReadMessage inc, MessageFlag flag) + { + NetCrewMember crewMember = INetSerializableStruct.Read(inc); + InsertPendingCrewMember(crewMember); + if (flag == MessageFlag.Response && TryDequeue(addRequests, out var callback)) + { + callback(new CallbackOnlyRequest(RequestResult.Success)); + } + OnUpdate?.Invoke(); + } + + private void NewRemovalReceived(IReadMessage inc, MessageFlag flag) + { + NetRemovedAffliction removed = INetSerializableStruct.Read(inc); + RemovePendingAffliction(removed.CrewMember, removed.Affliction); + if (flag == MessageFlag.Response && TryDequeue(removeRequests, out var callback)) + { + callback(new CallbackOnlyRequest(RequestResult.Success)); + } + OnUpdate?.Invoke(); + } + + private static void SendAfflictionRequest(CharacterInfo info) + { + INetSerializableStruct crewMember = new NetCrewMember + { + CharacterInfo = info, + Afflictions = Array.Empty() + }; + + ClientSend(crewMember, NetworkHeader.REQUEST_AFFLICTIONS, DeliveryMethod.Unreliable); + } + + private static void SendPendingRequest() + { + ClientSend(null, NetworkHeader.REQUEST_PENDING, DeliveryMethod.Reliable); + } + + private void AfflictionRequestReceived(IReadMessage inc) + { + NetCrewMember crewMember = INetSerializableStruct.Read(inc); + if (TryDequeue(afflictionRequests, out var callback)) + { + RequestResult result = crewMember.CharacterInfoID == 0 ? RequestResult.Error : RequestResult.Success; + callback(new AfflictionRequest(result, crewMember.Afflictions.ToImmutableArray())); + } + } + + private void PendingRequestReceived(IReadMessage inc) + { + NetPendingCrew pendingCrew = INetSerializableStruct.Read(inc); + if (TryDequeue(pendingHealRequests, out var callback)) + { + callback(new PendingRequest(RequestResult.Success, pendingCrew.CrewMembers.ToImmutableArray())); + } + } + + private static IWriteMessage StartSending() + { + IWriteMessage writeMessage = new WriteOnlyMessage(); + writeMessage.Write((byte)ClientPacketHeader.MEDICAL); + return writeMessage; + } + + private static void ClientSend(INetSerializableStruct? netStruct, NetworkHeader header, DeliveryMethod deliveryMethod) + { + IWriteMessage msg = StartSending(); + msg.Write((byte)header); + netStruct?.Write(msg); + GameMain.Client.ClientPeer?.Send(msg, deliveryMethod); + } + + public void ClientRead(IReadMessage inc) + { + NetworkHeader header = (NetworkHeader)inc.ReadByte(); + MessageFlag flag = (MessageFlag)inc.ReadByte(); + + switch (header) + { + case NetworkHeader.REQUEST_AFFLICTIONS: + AfflictionRequestReceived(inc); + break; + case NetworkHeader.REQUEST_PENDING: + PendingRequestReceived(inc); + break; + case NetworkHeader.ADD_PENDING: + NewAdditonReceived(inc, flag); + break; + case NetworkHeader.REMOVE_PENDING: + NewRemovalReceived(inc, flag); + break; + case NetworkHeader.HEAL_PENDING: + HealRequestReceived(inc); + break; + case NetworkHeader.CLEAR_PENDING: + ClearRequstReceived(); + break; + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs index bb01da050..e6d7114bc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/ReadyCheck.cs @@ -31,7 +31,7 @@ namespace Barotrauma private GUIMessageBox? msgBox; private GUIMessageBox? resultsBox; - public static DateTime lastReadyCheck = DateTime.MinValue; + public static DateTime ReadyCheckCooldown = DateTime.MinValue; public static bool IsReadyCheck(GUIComponent? msgBox) => msgBox?.UserData as string == PromptData || msgBox?.UserData as string == ResultData; @@ -273,10 +273,10 @@ namespace Barotrauma public static void CreateReadyCheck() { - if (lastReadyCheck < DateTime.Now) + if (ReadyCheckCooldown < DateTime.Now) { #if !DEBUG - lastReadyCheck = DateTime.Now.AddMinutes(1); + ReadyCheckCooldown = DateTime.Now.AddMinutes(1); #endif IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte) ClientPacketHeader.READY_CHECK); @@ -285,7 +285,7 @@ namespace Barotrauma return; } - GUIMessageBox msgBox = new GUIMessageBox(readyCheckHeader, readyCheckPleaseWait((lastReadyCheck - DateTime.Now).Seconds), new[] { closeButton }); + GUIMessageBox msgBox = new GUIMessageBox(readyCheckHeader, readyCheckPleaseWait((ReadyCheckCooldown - DateTime.Now).Seconds), new[] { closeButton }); msgBox.Buttons[0].OnClicked = delegate { msgBox.Close(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs index 251487afb..a71217c8d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs @@ -524,6 +524,7 @@ namespace Barotrauma return true; }; +#if !OSX var statisticsTickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.045f), leftPanel.RectTransform), TextManager.Get("statisticsconsenttickbox")) { OnSelected = (GUITickBox tickBox) => @@ -562,6 +563,8 @@ namespace Barotrauma statisticsTickBox.OnSelected = prevHandler; statisticsTickBox.Enabled = GameAnalyticsManager.UserConsented != GameAnalyticsManager.Consent.Error; }); +#endif + // right panel -------------------------------------- diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 0fe573f07..f7bc9048c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -504,6 +504,13 @@ namespace Barotrauma { HUDLayoutSettings.InventoryTopY = visualSlots[0].EquipButtonRect.Y - (int)(15 * GUI.Scale); } + else + { + for (int i = 0; i < capacity; i++) + { + visualSlots[i].DrawOffset = Vector2.Zero; + } + } } protected override void ControlInput(Camera cam) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/EntitySpawnerComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/EntitySpawnerComponent.cs index cd8d21dcf..97c661961 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/EntitySpawnerComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/EntitySpawnerComponent.cs @@ -28,8 +28,8 @@ namespace Barotrauma.Items.Components } case AreaShape.Circle: Vector2 center = item.WorldPosition; - center.Y = -center.Y; center += SpawnAreaOffset; + center.Y = -center.Y; spriteBatch.DrawCircle(center, SpawnAreaRadius, 32, GUI.Style.Red, thickness: 4f); if (MaximumAmountRangePadding > 0f) @@ -51,8 +51,8 @@ namespace Barotrauma.Items.Components } case AreaShape.Circle: Vector2 center = item.WorldPosition; - center.Y = -center.Y; center += CrewAreaOffset; + center.Y = -center.Y; spriteBatch.DrawCircle(center, CrewAreaRadius, 32, GUI.Style.Green); break; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index f42e06aa0..0e4b9b743 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -390,10 +390,10 @@ namespace Barotrauma.Items.Components if (SerializableProperties.TryGetValue(sound.VolumeProperty, out SerializableProperty property)) { - float newVolume = 0.0f; + float newVolume; try { - newVolume = (float)property.GetValue(this); + newVolume = property.GetFloatValue(this); } catch { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index ec2074a32..c2a3d7957 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -40,7 +40,7 @@ namespace Barotrauma.Items.Components } } - partial void SetLightSourceTransform() + partial void SetLightSourceTransformProjSpecific() { if (ParentBody != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs index 994deb666..0eb2cbf58 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Controller.cs @@ -6,21 +6,23 @@ namespace Barotrauma.Items.Components { partial class Controller : ItemComponent { + private bool chatBoxOriginalState; + private bool isHUDsHidden; + public override void DrawHUD(SpriteBatch spriteBatch, Character character) { if (focusTarget != null && character.ViewTarget == focusTarget) { foreach (ItemComponent ic in focusTarget.Components) { - ic.DrawHUD(spriteBatch, character); + if (ic.ShouldDrawHUD(character)) + { + ic.DrawHUD(spriteBatch, character); + } } } } - private bool crewAreaOriginalState; - private bool chatBoxOriginalState; - private bool isHUDsHidden; - partial void HideHUDs(bool value) { if (isHUDsHidden == value) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index e839046dc..57217ed9f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -302,6 +302,12 @@ namespace Barotrauma.Items.Components } HideEmptyItemListCategories(); + + if (selectedItem != null) + { + //reselect to recreate the info based on the new user's skills + SelectItem(character, selectedItem); + } } private void DrawInputOverLay(SpriteBatch spriteBatch, GUICustomComponent overlayComponent) @@ -343,13 +349,14 @@ namespace Barotrauma.Items.Components foreach (Item it in availableItems) { if (it.ParentInventory == inputContainer.Inventory) { continue; } - var rootContainer = it.GetRootContainer(); - if (rootContainer?.OwnInventory?.visualSlots == null) { continue; } - int availableSlotIndex = rootContainer.OwnInventory.FindIndex(it.Container == rootContainer ? it : it.Container); + var rootInventoryOwner = it.GetRootInventoryOwner(); + Inventory rootInventory = (rootInventoryOwner as Item)?.OwnInventory as Inventory ?? (rootInventoryOwner as Character)?.Inventory; + if (rootInventory?.visualSlots == null) { continue; } + int availableSlotIndex = rootInventory.FindIndex((it.Container != rootInventoryOwner ? it.Container : it) ?? it); if (availableSlotIndex < 0) { continue; } - if (rootContainer.OwnInventory.visualSlots[availableSlotIndex].HighlightTimer <= 0.0f) + if (rootInventory.visualSlots[availableSlotIndex].HighlightTimer <= 0.0f) { - rootContainer.OwnInventory.visualSlots[availableSlotIndex].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f, 0.2f); + rootInventory.visualSlots[availableSlotIndex].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f, 0.2f); if (slotIndex < inputContainer.Capacity) { inputContainer.Inventory.visualSlots[slotIndex].ShowBorderHighlight(GUI.Style.Green, 0.5f, 0.5f, 0.2f); @@ -406,9 +413,16 @@ namespace Barotrauma.Items.Components { toolTipText += " " + (int)Math.Round(requiredItem.MinCondition * 100) + "%"; } - else if(requiredItem.MaxCondition < 1.0f) + else if (requiredItem.MaxCondition < 1.0f) { - toolTipText += " 0-" + (int)Math.Round(requiredItem.MaxCondition * 100) + "%"; + if (requiredItem.MaxCondition <= 0.0f) + { + toolTipText += " " + (int)Math.Round(requiredItem.MaxCondition * 100) + "%"; + } + else + { + toolTipText += " 0-" + (int)Math.Round(requiredItem.MaxCondition * 100) + "%"; + } } else if (requiredItem.MaxCondition <= 0.0f) { @@ -524,16 +538,6 @@ namespace Barotrauma.Items.Components var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.9f), selectedItemFrame.RectTransform, Anchor.Center)) { RelativeSpacing = 0.03f }; var paddedReqFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.9f), selectedItemReqsFrame.RectTransform, Anchor.Center)) { RelativeSpacing = 0.03f }; - /*var itemIcon = selectedItem.TargetItem.InventoryIcon ?? selectedItem.TargetItem.sprite; - if (itemIcon != null) - { - GUIImage img = new GUIImage(new RectTransform(new Point(40, 40), paddedFrame.RectTransform), - itemIcon, scaleToFit: true) - { - Color = selectedItem.TargetItem.InventoryIconColor - }; - }*/ - string itemName = GetRecipeNameAndAmount(selectedItem); string name = itemName; @@ -732,8 +736,6 @@ namespace Barotrauma.Items.Components Character user = Entity.FindEntityByID(userID) as Character; State = newState; - timeUntilReady = newTimeUntilReady; - if (newState == FabricatorState.Stopped || itemIndex == -1) { CancelFabricating(); @@ -747,6 +749,7 @@ namespace Barotrauma.Items.Components SelectItem(user, fabricationRecipes[itemIndex]); StartFabricating(fabricationRecipes[itemIndex], user); } + timeUntilReady = newTimeUntilReady; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index 8bda33a7a..ace0a070e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -537,9 +537,9 @@ namespace Barotrauma.Items.Components if (item.Submarine == null && displayedSubs.Count > 0 || // item not inside a sub anymore, but display is still showing subs item.Submarine is { } itemSub && ( - !displayedSubs.Contains(itemSub) || // current sub not displayed - itemSub.DockedTo.Any(s => !displayedSubs.Contains(s)) || // some of the docked subs not displayed - displayedSubs.Any(s => s != itemSub && !itemSub.DockedTo.Contains(s)) // displaying a sub that shouldn't be displayed + !displayedSubs.Contains(itemSub) || // current sub not displayed + itemSub.DockedTo.Any(s => !displayedSubs.Contains(s) && itemSub.ConnectedDockingPorts[s].IsLocked) || // some of the docked subs not displayed + displayedSubs.Any(s => s != itemSub && !itemSub.DockedTo.Contains(s)) // displaying a sub that shouldn't be displayed ) || prevResolution.X != GameMain.GraphicsWidth || prevResolution.Y != GameMain.GraphicsHeight || // resolution changed !submarineContainer.Children.Any()) // We lack a GUI @@ -1092,6 +1092,12 @@ namespace Barotrauma.Items.Components if (!(entity is Item it)) { continue; } if (!electricalChildren.TryGetValue(miniMapGuiComponent, out GUIComponent component)) { continue; } + if (entity.Removed) + { + component.Visible = false; + continue; + } + if (item.Submarine == null || !hasPower) { component.Color = component.OutlineColor = NoPowerElectricalColor; @@ -1117,7 +1123,7 @@ namespace Barotrauma.Items.Components int current = (int)-powerTransfer.CurrPowerConsumption, load = (int)powerTransfer.PowerLoad; line1 = TextManager.GetWithVariable("statusmonitor.junctionpower.tooltip", "[amount]", current.ToString(), fallBackTag: "statusmonitor.junctioncurrent.tooltip"); - line2 = TextManager.GetWithVariable("statusmonitor.junctionload.tooltip", "[amount]", load.ToString()); + line2 = TextManager.GetWithVariables("statusmonitor.junctionload.tooltip", new string[] { "[amount]", "[load]" }, new string[] { load.ToString(), load.ToString() }); } string line3 = TextManager.GetWithVariable("statusmonitor.durability.tooltip", "[amount]", durability.ToString()); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs index a6b0b74b8..d0bf73438 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs @@ -19,15 +19,6 @@ namespace Barotrauma.Items.Components private readonly List<(Vector2 position, ParticleEmitter emitter)> pumpOutEmitters = new List<(Vector2 position, ParticleEmitter emitter)>(); private readonly List<(Vector2 position, ParticleEmitter emitter)> pumpInEmitters = new List<(Vector2 position, ParticleEmitter emitter)>(); - public float CurrentBrokenVolume - { - get - { - if (item.ConditionPercentage > 10.0f || !IsActive) { return 0.0f; } - return (1.0f - item.ConditionPercentage / 10.0f) * 100.0f; - } - } - partial void InitProjSpecific(XElement element) { foreach (XElement subElement in element.Elements()) @@ -193,8 +184,6 @@ namespace Barotrauma.Items.Components private readonly float flickerFrequency = 1; public override void UpdateHUD(Character character, float deltaTime, Camera cam) { - pumpSpeedLockTimer -= deltaTime; - isActiveLockTimer -= deltaTime; autoControlIndicator.Selected = IsAutoControlled; PowerButton.Enabled = isActiveLockTimer <= 0.0f; if (HasPower) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index d8831b9db..be316aff4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -83,9 +83,7 @@ namespace Barotrauma.Items.Components private const float ConnectedSubUpdateInterval = 1.0f; float connectedSubUpdateTimer; - //Vector2 = vector from the ping source to the position of the disruption - //float = strength of the disruption, between 0-1 - private readonly List> disruptedDirections = new List>(); + private readonly List<(Vector2 pos, float strength)> disruptedDirections = new List<(Vector2 pos, float strength)>(); private readonly Dictionary markerDistances = new Dictionary(); @@ -455,7 +453,20 @@ namespace Barotrauma.Items.Components zoomSlider.OnMoved(zoomSlider, zoomSlider.BarScroll); } } - + + Vector2 transducerCenter = GetTransducerPos(); + + if (steering != null && steering.DockingModeEnabled && steering.ActiveDockingSource != null) + { + Vector2 worldFocusPos = (steering.ActiveDockingSource.Item.WorldPosition + steering.DockingTarget.Item.WorldPosition) / 2.0f; + DisplayOffset = Vector2.Lerp(DisplayOffset, worldFocusPos - transducerCenter, 0.1f); + } + else + { + DisplayOffset = Vector2.Lerp(DisplayOffset, Vector2.Zero, 0.1f); + } + transducerCenter += DisplayOffset; + float distort = MathHelper.Clamp(1.0f - item.Condition / item.MaxCondition, 0.0f, 1.0f); for (int i = sonarBlips.Count - 1; i >= 0; i--) { @@ -502,8 +513,6 @@ namespace Barotrauma.Items.Components return; } - Vector2 transducerCenter = GetTransducerPos() + DisplayOffset; - if (Level.Loaded != null) { nearbyObjectUpdateTimer -= deltaTime; @@ -829,8 +838,7 @@ namespace Barotrauma.Items.Components } } - Vector2 transducerCenter = GetTransducerPos(); - + Vector2 transducerCenter = GetTransducerPos();// + DisplayOffset; if (sonarBlips.Count > 0) { @@ -840,7 +848,7 @@ namespace Barotrauma.Items.Components foreach (SonarBlip sonarBlip in sonarBlips) { - DrawBlip(spriteBatch, sonarBlip, transducerCenter, center, sonarBlip.FadeTimer / 2.0f * signalStrength, blipScale); + DrawBlip(spriteBatch, sonarBlip, transducerCenter + DisplayOffset, center, sonarBlip.FadeTimer / 2.0f * signalStrength, blipScale); } spriteBatch.End(); @@ -849,8 +857,8 @@ namespace Barotrauma.Items.Components if (item.Submarine != null && !DetectSubmarineWalls) { - DrawDockingPorts(spriteBatch, transducerCenter, signalStrength); transducerCenter += DisplayOffset; + DrawDockingPorts(spriteBatch, transducerCenter, signalStrength); DrawOwnSubmarineBorders(spriteBatch, transducerCenter, signalStrength); } else @@ -1083,10 +1091,6 @@ namespace Barotrauma.Items.Components { DrawDockingIndicator(spriteBatch, steering, ref transducerCenter); } - else - { - DisplayOffset = Vector2.Lerp(DisplayOffset, Vector2.Zero, 0.1f); - } foreach (DockingPort dockingPort in DockingPort.List) { @@ -1131,9 +1135,6 @@ namespace Barotrauma.Items.Components Vector2 worldFocusPos = (steering.ActiveDockingSource.Item.WorldPosition + steering.DockingTarget.Item.WorldPosition) / 2.0f; worldFocusPos.X = steering.DockingTarget.Item.WorldPosition.X; - DisplayOffset = Vector2.Lerp(DisplayOffset, worldFocusPos - transducerCenter, 0.1f); - transducerCenter += DisplayOffset; - Vector2 sourcePortDiff = (steering.ActiveDockingSource.Item.WorldPosition - transducerCenter) * scale; Vector2 sourcePortPos = new Vector2(sourcePortDiff.X, -sourcePortDiff.Y); Vector2 targetPortDiff = (steering.DockingTarget.Item.WorldPosition - transducerCenter) * scale; @@ -1234,7 +1235,7 @@ namespace Barotrauma.Items.Components Vector2 disruptionPos = new Vector2(levelObject.Position.X, levelObject.Position.Y); float disruptionDist = Vector2.Distance(pingSource, disruptionPos); - disruptedDirections.Add(new Pair((disruptionPos - pingSource) / disruptionDist, disruptionStrength)); + disruptedDirections.Add(((disruptionPos - pingSource) / disruptionDist, disruptionStrength)); CreateBlipsForDisruption(disruptionPos, disruptionStrength); @@ -1246,7 +1247,7 @@ namespace Barotrauma.Items.Components 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)); + disruptedDirections.Add(((aiTarget.WorldPosition - pingSource) / disruptionDist, aiTarget.SonarDisruption)); CreateBlipsForDisruption(aiTarget.WorldPosition, disruption); } } @@ -1461,10 +1462,10 @@ namespace Barotrauma.Items.Components float transducerDist = transducerDiff.Length(); Vector2 pingDirection = transducerDiff / transducerDist; bool disrupted = false; - foreach (Pair disruptDir in disruptedDirections) + foreach ((Vector2 disruptPos, float disruptStrength) in disruptedDirections) { - float dot = Vector2.Dot(pingDirection, disruptDir.First); - if (dot > 1.0f - disruptDir.Second) + float dot = Vector2.Dot(pingDirection, disruptPos); + if (dot > 1.0f - disruptStrength) { disrupted = true; break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index b66bcaa5a..be62c6110 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -403,7 +403,7 @@ namespace Barotrauma.Items.Components { if (GameMain.Client == null) { - item.SendSignal("1", "toggle_docking"); + item.SendSignal(new Signal("1", sender: Character.Controlled), "toggle_docking"); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index 2c2e02a2b..87f0dbe07 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -434,6 +434,7 @@ namespace Barotrauma.Items.Components DeteriorateAlways = msg.ReadBoolean(); tinkeringDuration = msg.ReadSingle(); tinkeringStrength = msg.ReadSingle(); + tinkeringPowersDevices = msg.ReadBoolean(); ushort currentFixerID = msg.ReadUInt16(); currentFixerAction = (FixActions)msg.ReadRangedInteger(0, 2); CurrentFixer = currentFixerID != 0 ? Entity.FindEntityByID(currentFixerID) as Character : null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs index ff50d0fef..d0a181824 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/ButtonTerminal.cs @@ -46,6 +46,7 @@ namespace Barotrauma.Items.Components child.Enabled = buttonsEnabled; child.Children.ForEach(c => c.Enabled = buttonsEnabled); } + if (Container == null) { return; } bool itemsContained = Container.Inventory.AllItems.Any(); if (itemsContained) { @@ -77,7 +78,7 @@ namespace Barotrauma.Items.Components { if (GameMain.IsSingleplayer) { - SendSignal((int)userData); + SendSignal((int)userData, Character.Controlled); } else { @@ -98,6 +99,7 @@ namespace Barotrauma.Items.Components partial void OnItemLoadedProjSpecific() { + if (Container == null) { return; } Container.AllowUIOverlap = true; Container.Inventory.RectTransform = containerHolder.RectTransform; } @@ -109,7 +111,7 @@ namespace Barotrauma.Items.Components public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { - SendSignal(msg.ReadRangedInteger(0, Signals.Length - 1), isServerMessage: true); + SendSignal(msg.ReadRangedInteger(0, Signals.Length - 1), sender: null, isServerMessage: true); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs index 365680a44..5a24346cc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs @@ -197,7 +197,7 @@ namespace Barotrauma.Items.Components } } - partial void UpdateProjSpecific() + public override void UpdateHUD(Character character, float deltaTime, Camera cam) { bool elementVisibilityChanged = false; int visibleElementCount = 0; @@ -209,6 +209,7 @@ namespace Barotrauma.Items.Components if (uiElement.Visible != visible) { uiElement.Visible = visible; + uiElement.IgnoreLayoutGroups = !uiElement.Visible; elementVisibilityChanged = true; } } @@ -223,6 +224,7 @@ namespace Barotrauma.Items.Components uiElement.RectTransform.RelativeSize = new Vector2(1.0f, elementSize); } GuiFrame.Visible = visibleElementCount > 0; + uiElementContainer.Recalculate(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/MotionSensor.cs index 08dd799bc..8d7ec8a22 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/MotionSensor.cs @@ -12,9 +12,9 @@ namespace Barotrauma.Items.Components public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) { - if (!editing || !MapEntity.SelectedList.Contains(item)) return; + if (!editing || !MapEntity.SelectedList.Contains(item)) { return; } - Vector2 pos = item.WorldPosition + detectOffset; + Vector2 pos = item.WorldPosition + TransformedDetectOffset; pos.Y = -pos.Y; GUI.DrawRectangle(spriteBatch, pos - new Vector2(rangeX, rangeY), new Vector2(rangeX, rangeY) * 2.0f, Color.Cyan * 0.5f, isFilled: false, thickness: 2); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs index f83259306..e0b26ad02 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs @@ -286,7 +286,6 @@ namespace Barotrauma.Items.Components item.Color, depth, 0.3f); } - public static void UpdateEditing(List wires) { var doubleClicked = PlayerInput.DoubleClicked(); @@ -509,6 +508,31 @@ namespace Barotrauma.Items.Components } } + public override void Move(Vector2 amount) + { + //only used in the sub editor, hence only in the client project + if (!item.IsSelected) { return; } + + Vector2 wireNodeOffset = item.Submarine == null ? Vector2.Zero : item.Submarine.HiddenSubPosition + amount; + for (int i = 0; i < nodes.Count; i++) + { + if (i == 0 || i == nodes.Count - 1) + { + if (connections[0]?.Item != null && !connections[0].Item.IsSelected && + (Submarine.RectContains(connections[0].Item.Rect, nodes[i] + wireNodeOffset) || Submarine.RectContains(connections[0].Item.Rect, nodes[i] + wireNodeOffset - amount))) + { + continue; + } + else if (connections[1]?.Item != null && !connections[1].Item.IsSelected && + (Submarine.RectContains(connections[1].Item.Rect, nodes[i] + wireNodeOffset) || Submarine.RectContains(connections[1].Item.Rect, nodes[i] + wireNodeOffset - amount))) + { + continue; + } + } + nodes[i] += amount; + } + UpdateSections(); + } public bool IsMouseOn() { if (GUI.MouseOn == null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs index d974c2044..88367f1a0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/StatusHUD.cs @@ -301,7 +301,7 @@ namespace Barotrauma.Items.Components Dictionary combinedAfflictionStrengths = new Dictionary(); foreach (Affliction affliction in allAfflictions) { - if (affliction.Strength < affliction.Prefab.ShowInHealthScannerThreshold || affliction.Strength <= 0.0f) continue; + if (affliction.Strength < affliction.Prefab.ShowInHealthScannerThreshold || affliction.Strength <= 0.0f) { continue; } if (combinedAfflictionStrengths.ContainsKey(affliction.Prefab)) { combinedAfflictionStrengths[affliction.Prefab] += affliction.Strength; @@ -314,7 +314,7 @@ namespace Barotrauma.Items.Components foreach (AfflictionPrefab affliction in combinedAfflictionStrengths.Keys) { - texts.Add(TextManager.AddPunctuation(':', affliction.Name, Math.Max(((int)combinedAfflictionStrengths[affliction]), 1).ToString() + " %")); + texts.Add(TextManager.AddPunctuation(':', affliction.Name, Math.Max((int)combinedAfflictionStrengths[affliction], 1).ToString() + " %")); textColors.Add(Color.Lerp(GUI.Style.Orange, GUI.Style.Red, combinedAfflictionStrengths[affliction] / affliction.MaxStrength)); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 60d8a0fc6..f5a43c09e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -17,7 +17,7 @@ namespace Barotrauma partial class Item : MapEntity, IDamageable, ISerializableEntity, IServerSerializable, IClientSerializable { public static bool ShowItems = true, ShowWires = true; - + private readonly List positionBuffer = new List(); private readonly List activeHUDs = new List(); @@ -89,8 +89,8 @@ namespace Barotrauma { if (itemInUseWarning == null) { - itemInUseWarning = new GUITextBlock(new RectTransform(new Point(10), GUI.Canvas), "", - textColor: GUI.Style.Orange, color: Color.Black, + itemInUseWarning = new GUITextBlock(new RectTransform(new Point(10), GUI.Canvas), "", + textColor: GUI.Style.Orange, color: Color.Black, textAlignment: Alignment.Center, style: "OuterGlow"); } return itemInUseWarning; @@ -105,6 +105,9 @@ namespace Barotrauma { return false; } + + if (!SubEditorScreen.IsLayerVisible(this)) { return false;} + return parentInventory == null && (body == null || body.Enabled) && ShowItems; } } @@ -154,7 +157,7 @@ namespace Barotrauma if (containedSprite.UseWhenAttached) { activeContainedSprite = containedSprite; - activeSprite = containedSprite.Sprite; + activeSprite = containedSprite.Sprite; UpdateSpriteStates(0.0f); return; } @@ -196,7 +199,7 @@ namespace Barotrauma { brokenSprite.Sprite.EnsureLazyLoaded(); } - + foreach (var decorativeSprite in ((ItemPrefab)prefab).DecorativeSprites) { decorativeSprite.Sprite.EnsureLazyLoaded(); @@ -255,7 +258,7 @@ namespace Barotrauma public override void Draw(SpriteBatch spriteBatch, bool editing, bool back = true) { - if (!Visible || (!editing && HiddenInGame)) { return; } + if (!Visible || (!editing && HiddenInGame) || !SubEditorScreen.IsLayerVisible(this)) { return; } if (editing) { @@ -265,7 +268,7 @@ namespace Barotrauma } else if (!ShowItems) { return; } } - + Color color = IsIncludedInSelection && editing ? GUI.Style.Blue : IsHighlighted && !GUI.DisableItemHighlights && Screen.Selected != GameMain.GameScreen ? GUI.Style.Orange * Math.Max(GetSpriteColor().A / (float) byte.MaxValue, 0.1f) : GetSpriteColor(); //if (IsSelected && editing) color = Color.Lerp(color, Color.Gold, 0.5f); @@ -273,7 +276,7 @@ namespace Barotrauma bool isWiringMode = editing && SubEditorScreen.TransparentWiringMode && SubEditorScreen.IsWiringMode() && !isWire && parentInventory == null; bool renderTransparent = isWiringMode && GetComponent() == null; if (renderTransparent) { color *= 0.15f; } - + BrokenItemSprite fadeInBrokenSprite = null; float fadeInBrokenSpriteAlpha = 0.0f; float displayCondition = FakeBroken ? 0.0f : ConditionPercentage; @@ -322,7 +325,7 @@ namespace Barotrauma Vector2 size = new Vector2(rect.Width, rect.Height); if (color.A > 0) { - activeSprite.DrawTiled(spriteBatch, new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)) + drawOffset, + activeSprite.DrawTiled(spriteBatch, new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)) + drawOffset, size, color: color, textureScale: Vector2.One * Scale, depth: depth); @@ -336,11 +339,11 @@ namespace Barotrauma } foreach (var decorativeSprite in Prefab.DecorativeSprites) { - if (!spriteAnimState[decorativeSprite].IsActive) { continue; } + if (!spriteAnimState[decorativeSprite].IsActive) { continue; } Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, flippedX && Prefab.CanSpriteFlipX ? rotationRad : -rotationRad) * Scale; if (flippedX && Prefab.CanSpriteFlipX) { offset.X = -offset.X; } if (flippedY && Prefab.CanSpriteFlipY) { offset.Y = -offset.Y; } - decorativeSprite.Sprite.DrawTiled(spriteBatch, + decorativeSprite.Sprite.DrawTiled(spriteBatch, new Vector2(DrawPosition.X + offset.X - rect.Width / 2, -(DrawPosition.Y + offset.Y + rect.Height / 2)), size, color: color, textureScale: Vector2.One * Scale, @@ -380,7 +383,7 @@ namespace Barotrauma Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, flippedX && Prefab.CanSpriteFlipX ? rotationRad : -rotationRad) * Scale; if (flippedX && Prefab.CanSpriteFlipX) { offset.X = -offset.X; } if (flippedY && Prefab.CanSpriteFlipY) { offset.Y = -offset.Y; } - decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color, + decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color, rotationRad + rot, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects, depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth), 0.999f)); } @@ -440,11 +443,11 @@ namespace Barotrauma depth: depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth)); } } - + foreach (var upgrade in Upgrades) { var upgradeSprites = GetUpgradeSprites(upgrade); - + foreach (var decorativeSprite in upgradeSprites) { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } @@ -456,7 +459,7 @@ namespace Barotrauma rotation, decorativeSprite.GetScale(spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects, depth: depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth)); } - + } activeSprite.effects = oldEffects; @@ -466,7 +469,7 @@ namespace Barotrauma } } - //use a backwards for loop because the drawable components may disable drawing, + //use a backwards for loop because the drawable components may disable drawing, //causing them to be removed from the list for (int i = drawableComponents.Count - 1; i >= 0; i--) { @@ -501,7 +504,7 @@ namespace Barotrauma Vector2 drawPos = new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)); Vector2 drawSize = new Vector2(MathF.Ceiling(rect.Width + Math.Abs(drawPos.X - (int)drawPos.X)), MathF.Ceiling(rect.Height + Math.Abs(drawPos.Y - (int)drawPos.Y))); drawPos = new Vector2(MathF.Floor(drawPos.X), MathF.Floor(drawPos.Y)); - GUI.DrawRectangle(spriteBatch, drawPos, drawSize, + GUI.DrawRectangle(spriteBatch, drawPos, drawSize, Color.White, false, 0, thickness: Math.Max(1, (int)(2 / Screen.Selected.Cam.Zoom))); foreach (Rectangle t in Prefab.Triggers) @@ -582,19 +585,25 @@ namespace Barotrauma } } - DecorativeSprite.UpdateSpriteStates(Prefab.DecorativeSpriteGroups, spriteAnimState, ID, deltaTime, ConditionalMatches); - + if (Prefab.DecorativeSpriteGroups.Count > 0) + { + DecorativeSprite.UpdateSpriteStates(Prefab.DecorativeSpriteGroups, spriteAnimState, ID, deltaTime, ConditionalMatches); + } + foreach (var upgrade in Upgrades) { - var upgradeSprites = GetUpgradeSprites(upgrade); + var upgradeSprites = GetUpgradeSprites(upgrade); foreach (var decorativeSprite in upgradeSprites) { var spriteState = spriteAnimState[decorativeSprite]; spriteState.IsActive = true; - foreach (var _ in decorativeSprite.IsActiveConditionals.Where(conditional => !ConditionalMatches(conditional))) + foreach (var conditional in decorativeSprite.IsActiveConditionals) { - spriteState.IsActive = false; - break; + if (!ConditionalMatches(conditional)) + { + spriteState.IsActive = false; + break; + } } } } @@ -696,8 +705,8 @@ namespace Barotrauma foreach (string tag in ip.PreferredContainers.SelectMany(pc => pc.Primary)) { availableTags.Add(tag); } foreach (string tag in ip.PreferredContainers.SelectMany(pc => pc.Secondary)) { availableTags.Add(tag); } } - //remove identifiers from the available container tags - //(otherwise the list will include many irrelevant options, + //remove identifiers from the available container tags + //(otherwise the list will include many irrelevant options, //e.g. "weldingtool" because a welding fuel tank can be placed inside the container, etc) availableTags.RemoveWhere(t => MapEntityPrefab.List.Any(me => me.Identifier == t)); new GUIButton(new RectTransform(new Vector2(0.1f, 1), tagsField.RectTransform, Anchor.TopRight), "...") @@ -749,7 +758,7 @@ namespace Barotrauma { me.FlipY(relativeToSub: false); } - if (!SelectedList.Contains(this)) { FlipY(relativeToSub: false); } + if (!SelectedList.Contains(this)) { FlipY(relativeToSub: false); } return true; } }; @@ -805,9 +814,9 @@ namespace Barotrauma { if (!ic.AllowInGameEditing) { continue; } if (SerializableProperty.GetProperties(ic).Count == 0 && - !SerializableProperty.GetProperties(ic).Any(p => p.GetAttribute().IsEditable(ic))) + !SerializableProperty.GetProperties(ic).Any(p => p.GetAttribute().IsEditable(ic))) { - continue; + continue; } } else @@ -869,7 +878,7 @@ namespace Barotrauma textBox.Text = relatedItem.JoinedIdentifiers; return true; }; - } + } ic.CreateEditingHUD(componentEditor); componentEditor.Recalculate(); @@ -892,7 +901,7 @@ namespace Barotrauma return upgradeSprites; } - + public override bool AddUpgrade(Upgrade upgrade, bool createNetworkEvent = false) { if (upgrade.Prefab.IsWallUpgrade) { return false; } @@ -949,7 +958,7 @@ namespace Barotrauma //reset positions first List elementsToMove = new List(); - if (editingHUD != null && editingHUD.UserData == this && + if (editingHUD != null && editingHUD.UserData == this && ((HasInGameEditableProperties && Character.Controlled?.SelectedConstruction == this) || Screen.Selected == GameMain.SubEditorScreen)) { elementsToMove.Add(editingHUD); @@ -972,8 +981,8 @@ namespace Barotrauma int disallowedPadding = (int)(50 * GUI.Scale); disallowedAreas.Add(GameMain.GameSession.CrewManager.GetActiveCrewArea()); disallowedAreas.Add(new Rectangle( - HUDLayoutSettings.ChatBoxArea.X - disallowedPadding, HUDLayoutSettings.ChatBoxArea.Y, - HUDLayoutSettings.ChatBoxArea.Width + disallowedPadding, HUDLayoutSettings.ChatBoxArea.Height)); + HUDLayoutSettings.ChatBoxArea.X - disallowedPadding, HUDLayoutSettings.ChatBoxArea.Y, + HUDLayoutSettings.ChatBoxArea.Width + disallowedPadding, HUDLayoutSettings.ChatBoxArea.Height)); } if (Screen.Selected is SubEditorScreen editor) @@ -985,8 +994,8 @@ namespace Barotrauma GUI.PreventElementOverlap(elementsToMove, disallowedAreas, new Rectangle( - 0, 20, - GameMain.GraphicsWidth, + 0, 20, + GameMain.GraphicsWidth, HUDLayoutSettings.InventoryTopY > 0 ? HUDLayoutSettings.InventoryTopY - 40 : GameMain.GraphicsHeight - 80)); foreach (ItemComponent ic in activeHUDs) @@ -995,7 +1004,7 @@ namespace Barotrauma var linkUIToComponent = ic.GetLinkUIToComponent(); - if (linkUIToComponent == null) { continue; } + if (linkUIToComponent == null) { continue; } ic.GuiFrame.RectTransform.ScreenSpaceOffset = linkUIToComponent.GuiFrame.RectTransform.ScreenSpaceOffset; } @@ -1110,14 +1119,14 @@ namespace Barotrauma } } } - + public void DrawHUD(SpriteBatch spriteBatch, Camera cam, Character character) { if (HasInGameEditableProperties && (character.SelectedConstruction == this || EditableWhenEquipped)) { DrawEditing(spriteBatch, cam); } - + foreach (ItemComponent ic in activeHUDs) { if (ic.CanBeSelected) @@ -1138,9 +1147,9 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, debugInitialHudPositions[i], Color.Orange); GUI.DrawRectangle(spriteBatch, ic.GuiFrame.Rect, Color.LightGreen); GUI.DrawLine(spriteBatch, debugInitialHudPositions[i].Location.ToVector2(), ic.GuiFrame.Rect.Location.ToVector2(), Color.Orange); - + i++; - } + } } } @@ -1262,7 +1271,7 @@ namespace Barotrauma NetEntityEvent.Type eventType = (NetEntityEvent.Type)msg.ReadRangedInteger(0, Enum.GetValues(typeof(NetEntityEvent.Type)).Length - 1); - + switch (eventType) { case NetEntityEvent.Type.ComponentState: @@ -1323,7 +1332,7 @@ namespace Barotrauma ItemComponent targetComponent = componentIndex < components.Count ? components[componentIndex] : null; Character targetCharacter = FindEntityByID(targetCharacterID) as Character; - Limb targetLimb = targetCharacter != null && targetLimbID < targetCharacter.AnimController.Limbs.Length ? + Limb targetLimb = targetCharacter != null && targetLimbID < targetCharacter.AnimController.Limbs.Length ? targetCharacter.AnimController.Limbs[targetLimbID] : null; Entity useTarget = FindEntityByID(useTargetID); @@ -1334,7 +1343,7 @@ namespace Barotrauma else { targetComponent.ApplyStatusEffects(actionType, 1.0f, targetCharacter, targetLimb, useTarget, worldPosition: worldPosition); - } + } } break; case NetEntityEvent.Type.ChangeProperty: @@ -1346,7 +1355,7 @@ namespace Barotrauma if (UpgradePrefab.Find(identifier) is { } upgradePrefab) { Upgrade upgrade = new Upgrade(this, upgradePrefab, level); - + byte targetCount = msg.ReadByte(); for (int i = 0; i < targetCount; i++) { @@ -1360,7 +1369,7 @@ namespace Barotrauma AddUpgrade(upgrade, false); } - break; + break; case NetEntityEvent.Type.Invalid: break; } @@ -1394,7 +1403,7 @@ namespace Barotrauma Character targetCharacter = FindEntityByID(characterID) as Character; msg.Write(characterID); - msg.Write(targetCharacter == null ? (byte)255 : (byte)Array.IndexOf(targetCharacter.AnimController.Limbs, targetLimb)); + msg.Write(targetCharacter == null ? (byte)255 : (byte)Array.IndexOf(targetCharacter.AnimController.Limbs, targetLimb)); break; case NetEntityEvent.Type.ChangeProperty: WritePropertyChange(msg, extraData, true); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs index 6eeda5f1f..430059ff6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs @@ -14,7 +14,7 @@ namespace Barotrauma { get { - return ShowGaps; + return ShowGaps && SubEditorScreen.IsLayerVisible(this); } } @@ -42,7 +42,7 @@ namespace Barotrauma } } - if (!editing || !ShowGaps) { return; } + if (!editing || !ShowGaps || !SubEditorScreen.IsLayerVisible(this)) { return; } Color clr = (open == 0.0f) ? GUI.Style.Red : Color.Cyan; if (IsHighlighted) clr = Color.Gold; @@ -128,8 +128,14 @@ namespace Barotrauma { //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; } + foreach (Hull h in hull1.linkedTo) + { + if (h.linkedTo.Contains(hull1) && h.linkedTo.Contains(hull2)) { return; } + } + foreach (Hull h in hull2.linkedTo) + { + if (h.linkedTo.Contains(hull1) && h.linkedTo.Contains(hull2)) { return; } + } } Vector2 pos = Position; @@ -177,7 +183,7 @@ namespace Barotrauma "bubbles", (Submarine == null ? pos : pos + Submarine.Position), velocity, 0, flowTargetHull); - + particleTimer -= emitInterval; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index 13533eb7d..d1d15b1c9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -34,14 +34,14 @@ namespace Barotrauma private readonly List remoteDecals = new List(); private readonly HashSet pendingDecalUpdates = new HashSet(); - + private double lastAmbientLightEditTime; public override bool SelectableInEditor { get { - return ShowHulls; + return ShowHulls && SubEditorScreen.IsLayerVisible(this); } } @@ -133,39 +133,41 @@ namespace Barotrauma else { if (!entity.linkedTo.Contains(this)) { entity.linkedTo.Add(this); } - if (!linkedTo.Contains(this)) { linkedTo.Add(entity); } + if (!linkedTo.Contains(this)) { linkedTo.Add(entity); } } } } partial void UpdateProjSpecific(float deltaTime, Camera cam) { - serverUpdateDelay -= deltaTime; - if (serverUpdateDelay <= 0.0f) + if (GameMain.Client != null) { - ApplyRemoteState(); - } - - if (networkUpdatePending) - { - networkUpdateTimer += deltaTime; - if (networkUpdateTimer > 0.2f) + serverUpdateDelay -= deltaTime; + if (serverUpdateDelay <= 0.0f) { - if (!pendingSectionUpdates.Any() && !pendingDecalUpdates.Any()) + ApplyRemoteState(); + } + if (networkUpdatePending) + { + networkUpdateTimer += deltaTime; + if (networkUpdateTimer > 0.2f) { - GameMain.NetworkMember?.CreateEntityEvent(this); + if (!pendingSectionUpdates.Any() && !pendingDecalUpdates.Any()) + { + GameMain.NetworkMember?.CreateEntityEvent(this); + } + foreach (Decal decal in pendingDecalUpdates) + { + GameMain.NetworkMember?.CreateEntityEvent(this, new object[] { decal }); + } + foreach (int pendingSectionUpdate in pendingSectionUpdates) + { + GameMain.NetworkMember?.CreateEntityEvent(this, new object[] { pendingSectionUpdate }); + } + pendingSectionUpdates.Clear(); + networkUpdatePending = false; + networkUpdateTimer = 0.0f; } - foreach (Decal decal in pendingDecalUpdates) - { - GameMain.NetworkMember?.CreateEntityEvent(this, new object[] { decal }); - } - foreach (int pendingSectionUpdate in pendingSectionUpdates) - { - GameMain.NetworkMember?.CreateEntityEvent(this, new object[] { pendingSectionUpdate }); - } - pendingSectionUpdates.Clear(); - networkUpdatePending = false; - networkUpdateTimer = 0.0f; } } @@ -243,7 +245,7 @@ namespace Barotrauma return; } - if (!ShowHulls && !GameMain.DebugDraw) { return; } + if ((!ShowHulls || !SubEditorScreen.IsLayerVisible(this)) && !GameMain.DebugDraw) { return; } if (!editing && (!GameMain.DebugDraw || Screen.Selected.Cam.Zoom < 0.1f)) { return; } @@ -385,45 +387,42 @@ namespace Barotrauma } } + private static readonly Vector3[] corners = new Vector3[6]; + private static readonly Vector2[] uvCoords = new Vector2[4]; + private static readonly Vector3[] prevCorners = new Vector3[2]; + private static readonly Vector2[] prevUVs = new Vector2[2]; + private void UpdateVertices(Camera cam, EntityGrid entityGrid, WaterRenderer renderer) { Vector2 submarinePos = Submarine == null ? Vector2.Zero : Submarine.DrawPosition; //if there's no more space in the buffer, don't render the water in the hull - //not an ideal solution, but this seems to only happen in cases where the missing + //not an ideal solution, but this seems to only happen in cases where the missing //water is not very noticeable (e.g. zoomed very far out so that multiple subs and ruins are visible) if (renderer.PositionInBuffer > renderer.vertices.Length - 6) { return; } - if (!renderer.IndoorsVertices.ContainsKey(entityGrid)) - { - renderer.IndoorsVertices[entityGrid] = new VertexPositionColorTexture[WaterRenderer.DefaultIndoorsBufferSize]; - renderer.PositionInIndoorsBuffer[entityGrid] = 0; - } - //calculate where the surface should be based on the water volume float top = rect.Y + submarinePos.Y; float bottom = top - rect.Height; float renderSurface = drawSurface + submarinePos.Y; - if (bottom > cam.WorldView.Y || top < cam.WorldView.Y - cam.WorldView.Height) return; + if (bottom > cam.WorldView.Y || top < cam.WorldView.Y - cam.WorldView.Height) { return; } + if (rect.X + submarinePos.X > cam.WorldView.Right || rect.Right + submarinePos.X < cam.WorldView.X) { return; } - Matrix transform = cam.Transform * Matrix.CreateOrthographic(GameMain.GraphicsWidth, GameMain.GraphicsHeight, -1, 1) * 0.5f; + Matrix transform = cam.Transform * Matrix.CreateOrthographic(GameMain.GraphicsWidth, GameMain.GraphicsHeight, -1, 1) * 0.5f; if (!update) { // create the four corners of our triangle. - Vector3[] corners = new Vector3[4]; - corners[0] = new Vector3(rect.X, rect.Y, 0.0f); corners[1] = new Vector3(rect.X + rect.Width, rect.Y, 0.0f); corners[2] = new Vector3(corners[1].X, rect.Y - rect.Height, 0.0f); corners[3] = new Vector3(corners[0].X, corners[2].Y, 0.0f); - Vector2[] uvCoords = new Vector2[4]; for (int i = 0; i < 4; i++) { corners[i] += new Vector3(submarinePos, 0.0f); @@ -443,6 +442,15 @@ namespace Barotrauma return; } + if (!renderer.IndoorsVertices.ContainsKey(entityGrid)) + { + renderer.IndoorsVertices[entityGrid] = new VertexPositionColorTexture[WaterRenderer.DefaultIndoorsBufferSize]; + } + if (!renderer.PositionInIndoorsBuffer.ContainsKey(entityGrid)) + { + renderer.PositionInIndoorsBuffer[entityGrid] = 0; + } + float x = rect.X; if (Submarine != null) { x += Submarine.DrawPosition.X; } @@ -454,20 +462,15 @@ namespace Barotrauma x += start * WaveWidth; - Vector3[] prevCorners = new Vector3[2]; - Vector2[] prevUVs = new Vector2[2]; - int width = WaveWidth; - + for (int i = start; i < end; i++) { - Vector3[] corners = new Vector3[6]; - //top left corners[0] = new Vector3(x, top, 0.0f); //watersurface left corners[3] = new Vector3(corners[0].X, renderSurface + waveY[i], 0.0f); - + //top right corners[1] = new Vector3(x + width, top, 0.0f); //watersurface right @@ -477,7 +480,7 @@ namespace Barotrauma corners[4] = new Vector3(x, bottom, 0.0f); //bottom right corners[5] = new Vector3(x + width, bottom, 0.0f); - + Vector2[] uvCoords = new Vector2[4]; for (int n = 0; n < 4; n++) { @@ -714,7 +717,7 @@ namespace Barotrauma } remoteBackgroundSections.Clear(); - if (remoteDecals.Any()) + if (remoteDecals.Count > 0) { decals.Clear(); foreach (RemoteDecal remoteDecal in remoteDecals) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs index dab22d962..71df6f48a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs @@ -51,7 +51,7 @@ namespace Barotrauma public readonly WaterVertexData IndoorsSurfaceBottomColor = new WaterVertexData(0.2f, 0.1f, 0.9f, 1.0f); public VertexPositionTexture[] vertices = new VertexPositionTexture[DefaultBufferSize]; - public Dictionary IndoorsVertices = new Dictionary();// VertexPositionColorTexture[DefaultBufferSize * 2]; + public Dictionary IndoorsVertices = new Dictionary(); public Effect WaterEffect { @@ -81,13 +81,17 @@ namespace Barotrauma if (basicEffect == null) { - basicEffect = new BasicEffect(GameMain.Instance.GraphicsDevice); - basicEffect.VertexColorEnabled = false; - - basicEffect.TextureEnabled = true; + basicEffect = new BasicEffect(GameMain.Instance.GraphicsDevice) + { + VertexColorEnabled = false, + TextureEnabled = true + }; } } + private readonly VertexPositionColorTexture[] tempVertices = new VertexPositionColorTexture[6]; + private readonly Vector3[] tempCorners = new Vector3[4]; + public void RenderWater(SpriteBatch spriteBatch, RenderTarget2D texture, Camera cam) { spriteBatch.GraphicsDevice.BlendState = BlendState.NonPremultiplied; @@ -139,29 +143,26 @@ namespace Barotrauma WaterEffect.CurrentTechnique.Passes[0].Apply(); - VertexPositionColorTexture[] verts = new VertexPositionColorTexture[6]; - Rectangle view = cam != null ? cam.WorldView : spriteBatch.GraphicsDevice.Viewport.Bounds; - var corners = new Vector3[4]; - corners[0] = new Vector3(view.X, view.Y, 0.1f); - corners[1] = new Vector3(view.Right, view.Y, 0.1f); - corners[2] = new Vector3(view.Right, view.Y - view.Height, 0.1f); - corners[3] = new Vector3(view.X, view.Y - view.Height, 0.1f); + tempCorners[0] = new Vector3(view.X, view.Y, 0.1f); + tempCorners[1] = new Vector3(view.Right, view.Y, 0.1f); + tempCorners[2] = new Vector3(view.Right, view.Y - view.Height, 0.1f); + tempCorners[3] = new Vector3(view.X, view.Y - view.Height, 0.1f); WaterVertexData backGroundColor = new WaterVertexData(0.1f, 0.1f, 0.5f, 1.0f); - verts[0] = new VertexPositionColorTexture(corners[0], backGroundColor, Vector2.Zero); - verts[1] = new VertexPositionColorTexture(corners[1], backGroundColor, Vector2.Zero); - verts[2] = new VertexPositionColorTexture(corners[2], backGroundColor, Vector2.Zero); - verts[3] = new VertexPositionColorTexture(corners[0], backGroundColor, Vector2.Zero); - verts[4] = new VertexPositionColorTexture(corners[2], backGroundColor, Vector2.Zero); - verts[5] = new VertexPositionColorTexture(corners[3], backGroundColor, Vector2.Zero); + tempVertices[0] = new VertexPositionColorTexture(tempCorners[0], backGroundColor, Vector2.Zero); + tempVertices[1] = new VertexPositionColorTexture(tempCorners[1], backGroundColor, Vector2.Zero); + tempVertices[2] = new VertexPositionColorTexture(tempCorners[2], backGroundColor, Vector2.Zero); + tempVertices[3] = new VertexPositionColorTexture(tempCorners[0], backGroundColor, Vector2.Zero); + tempVertices[4] = new VertexPositionColorTexture(tempCorners[2], backGroundColor, Vector2.Zero); + tempVertices[5] = new VertexPositionColorTexture(tempCorners[3], backGroundColor, Vector2.Zero); - spriteBatch.GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, verts, 0, 2); + spriteBatch.GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, tempVertices, 0, 2); foreach (KeyValuePair subVerts in IndoorsVertices) { - if (!PositionInIndoorsBuffer.ContainsKey(subVerts.Key) || PositionInIndoorsBuffer[subVerts.Key] == 0) continue; + if (!PositionInIndoorsBuffer.ContainsKey(subVerts.Key) || PositionInIndoorsBuffer[subVerts.Key] == 0) { continue; } offset = WavePos; if (subVerts.Key.Submarine != null) { offset -= subVerts.Key.Submarine.WorldPosition; } @@ -207,11 +208,23 @@ namespace Barotrauma basicEffect.CurrentTechnique.Passes[0].Apply(); } + private readonly List buffersToRemove = new List(); public void ResetBuffers() { PositionInBuffer = 0; PositionInIndoorsBuffer.Clear(); - IndoorsVertices.Clear(); + buffersToRemove.Clear(); + foreach (var buffer in IndoorsVertices.Keys) + { + if (buffer.Submarine?.Removed ?? false) + { + buffersToRemove.Add(buffer); + } + } + foreach (var bufferToRemove in buffersToRemove) + { + IndoorsVertices.Remove(bufferToRemove); + } } public void Dispose() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index a47285cca..151b148dd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -517,7 +517,7 @@ namespace Barotrauma.Lights private void RefreshConvexHullList(ConvexHullList chList, Vector2 lightPos, Submarine sub) { var fullChList = ConvexHull.HullLists.Find(x => x.Submarine == sub); - if (fullChList == null) return; + if (fullChList == null) { return; } chList.List = fullChList.List.FindAll(ch => ch.Enabled && MathUtils.CircleIntersectsRectangle(lightPos, TextureRange, ch.BoundingBox)); @@ -530,105 +530,121 @@ namespace Barotrauma.Lights /// private void CheckHullsInRange() { - List subs = new List(Submarine.Loaded); - subs.Add(null); - - foreach (Submarine sub in subs) + foreach (Submarine sub in Submarine.Loaded) { - //find the list of convexhulls that belong to the sub - var chList = hullsInRange.Find(x => x.Submarine == sub); + CheckHullsInRange(sub); + } + //check convex hulls that aren't in any sub + CheckHullsInRange(null); + } - //not found -> create one - if (chList == null) + private void CheckHullsInRange(Submarine sub) + { + //find the list of convexhulls that belong to the sub + ConvexHullList chList = null; + foreach (var ch in hullsInRange) + { + if (ch.Submarine == sub) { - chList = new ConvexHullList(sub); - hullsInRange.Add(chList); - NeedsRecalculation = true; - } - - if (chList.List.Any(ch => ch.LastVertexChangeTime > lastRecalculationTime && !chList.IsHidden.Contains(ch))) - { - NeedsRecalculation = true; - } - - Vector2 lightPos = position; - if (ParentSub == null) - { - //light and the convexhulls are both outside - if (sub == null) - { - if (NeedsHullCheck) - { - RefreshConvexHullList(chList, lightPos, null); - } - } - //light is outside, convexhulls inside a sub - else - { - lightPos -= sub.Position; - - Rectangle subBorders = sub.Borders; - subBorders.Location += sub.HiddenSubPosition.ToPoint() - new Point(0, sub.Borders.Height); - - //only draw if the light overlaps with the sub - if (!MathUtils.CircleIntersectsRectangle(lightPos, TextureRange, subBorders)) - { - if (chList.List.Count > 0) NeedsRecalculation = true; - chList.List.Clear(); - continue; - } - - RefreshConvexHullList(chList, lightPos, sub); - } - } - else - { - //light is inside, convexhull outside - if (sub == null) continue; - - //light and convexhull are both inside the same sub - if (sub == ParentSub) - { - if (NeedsHullCheck) - { - RefreshConvexHullList(chList, lightPos, sub); - } - } - //light and convexhull are inside different subs - else - { - if (sub.DockedTo.Contains(ParentSub) && !NeedsHullCheck) continue; - - lightPos -= (sub.Position - ParentSub.Position); - - Rectangle subBorders = sub.Borders; - subBorders.Location += sub.HiddenSubPosition.ToPoint() - new Point(0, sub.Borders.Height); - - //don't draw any shadows if the light doesn't overlap with the borders of the sub - if (!MathUtils.CircleIntersectsRectangle(lightPos, TextureRange, subBorders)) - { - if (chList.List.Count > 0) NeedsRecalculation = true; - chList.List.Clear(); - continue; - } - - //recalculate vertices if the subs have moved > 5 px relative to each other - Vector2 diff = ParentSub.WorldPosition - sub.WorldPosition; - if (!diffToSub.TryGetValue(sub, out Vector2 prevDiff)) - { - diffToSub.Add(sub, diff); - NeedsRecalculation = true; - } - else if (Vector2.DistanceSquared(diff, prevDiff) > 5.0f * 5.0f) - { - diffToSub[sub] = diff; - NeedsRecalculation = true; - } - - RefreshConvexHullList(chList, lightPos, sub); - } + chList = ch; + break; } } + + //not found -> create one + if (chList == null) + { + chList = new ConvexHullList(sub); + hullsInRange.Add(chList); + NeedsRecalculation = true; + } + + foreach (var ch in chList.List) + { + if (ch.LastVertexChangeTime > lastRecalculationTime && !chList.IsHidden.Contains(ch)) + { + NeedsRecalculation = true; + break; + } + } + + Vector2 lightPos = position; + if (ParentSub == null) + { + //light and the convexhulls are both outside + if (sub == null) + { + if (NeedsHullCheck) + { + RefreshConvexHullList(chList, lightPos, null); + } + } + //light is outside, convexhulls inside a sub + else + { + lightPos -= sub.Position; + + Rectangle subBorders = sub.Borders; + subBorders.Location += sub.HiddenSubPosition.ToPoint() - new Point(0, sub.Borders.Height); + + //only draw if the light overlaps with the sub + if (!MathUtils.CircleIntersectsRectangle(lightPos, TextureRange, subBorders)) + { + if (chList.List.Count > 0) { NeedsRecalculation = true; } + chList.List.Clear(); + return; + } + + RefreshConvexHullList(chList, lightPos, sub); + } + } + else + { + //light is inside, convexhull outside + if (sub == null) { return; } + + //light and convexhull are both inside the same sub + if (sub == ParentSub) + { + if (NeedsHullCheck) + { + RefreshConvexHullList(chList, lightPos, sub); + } + } + //light and convexhull are inside different subs + else + { + if (sub.DockedTo.Contains(ParentSub) && !NeedsHullCheck) { return; } + + lightPos -= (sub.Position - ParentSub.Position); + + Rectangle subBorders = sub.Borders; + subBorders.Location += sub.HiddenSubPosition.ToPoint() - new Point(0, sub.Borders.Height); + + //don't draw any shadows if the light doesn't overlap with the borders of the sub + if (!MathUtils.CircleIntersectsRectangle(lightPos, TextureRange, subBorders)) + { + if (chList.List.Count > 0) { NeedsRecalculation = true; } + chList.List.Clear(); + return; + } + + //recalculate vertices if the subs have moved > 5 px relative to each other + Vector2 diff = ParentSub.WorldPosition - sub.WorldPosition; + if (!diffToSub.TryGetValue(sub, out Vector2 prevDiff)) + { + diffToSub.Add(sub, diff); + NeedsRecalculation = true; + } + else if (Vector2.DistanceSquared(diff, prevDiff) > 5.0f * 5.0f) + { + diffToSub[sub] = diff; + NeedsRecalculation = true; + } + + RefreshConvexHullList(chList, lightPos, sub); + } + } } private List FindRaycastHits() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs index 4c96fb6a2..2dc444032 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs @@ -75,10 +75,12 @@ namespace Barotrauma if (linkedTo.Contains(entity)) { linkedTo.Remove(entity); + entity.linkedTo.Remove(this); } else { linkedTo.Add(entity); + if (!entity.linkedTo.Contains(this)) { entity.linkedTo.Add(this); } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index d8df8b2a9..55bba4cda 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -30,6 +30,9 @@ namespace Barotrauma { return false; } + + if (!SubEditorScreen.IsLayerVisible(this)) { return false; } + return HasBody ? ShowWalls : ShowStructures; } } @@ -244,8 +247,10 @@ namespace Barotrauma public override void Draw(SpriteBatch spriteBatch, bool editing, bool back = true) { if (prefab.sprite == null) { return; } + if (editing) { + if (!SubEditorScreen.IsLayerVisible(this)) { return; } if (!HasBody && !ShowStructures) { return; } if (HasBody && !ShowWalls) { return; } } @@ -273,6 +278,7 @@ namespace Barotrauma if (prefab.sprite == null) { return; } if (editing) { + if (!SubEditorScreen.IsLayerVisible(this)) { return; } if (!HasBody && !ShowStructures) { return; } if (HasBody && !ShowWalls) { return; } } @@ -285,13 +291,11 @@ namespace Barotrauma //color = Color.Lerp(color, Color.Gold, 0.5f); color = spriteColor; - - Vector2 rectSize = rect.Size.ToVector2(); if (BodyWidth > 0.0f) { rectSize.X = BodyWidth; } if (BodyHeight > 0.0f) { rectSize.Y = BodyHeight; } - Vector2 bodyPos = WorldPosition + BodyOffset; + Vector2 bodyPos = WorldPosition + BodyOffset * Scale; GUI.DrawRectangle(spriteBatch, new Vector2(bodyPos.X, -bodyPos.Y), rectSize.X, rectSize.Y, BodyRotation, Color.White, thickness: Math.Max(1, (int)(2 / Screen.Selected.Cam.Zoom))); @@ -465,7 +469,8 @@ namespace Barotrauma public void UpdateSpriteStates(float deltaTime) { - DecorativeSprite.UpdateSpriteStates(Prefab.DecorativeSpriteGroups, spriteAnimState, ID, deltaTime, ConditionalMatches); + if (Prefab.DecorativeSpriteGroups.Count == 0) { return; } + DecorativeSprite.UpdateSpriteStates(Prefab.DecorativeSpriteGroups, spriteAnimState, ID, deltaTime, ConditionalMatches); foreach (int spriteGroup in Prefab.DecorativeSpriteGroups.Keys) { for (int i = 0; i < Prefab.DecorativeSpriteGroups[spriteGroup].Count; i++) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index 2890e470f..9f35f13be 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -25,14 +25,11 @@ namespace Barotrauma public readonly bool Stream; public readonly bool IgnoreMuffling; - - public string Filename - { - get { return Sound?.Filename; } - } + public readonly string Filename; public RoundSound(XElement element, Sound sound) { + Filename = sound?.Filename; Sound = sound; Stream = sound.Stream; Range = element.GetAttributeFloat("range", 1000.0f); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs index 0696a24a2..6a2abf2aa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs @@ -146,6 +146,7 @@ namespace Barotrauma private bool IsHidden() { + if (!SubEditorScreen.IsLayerVisible(this)) { return false; } if (spawnType == SpawnType.Path) { return (!GameMain.DebugDraw && !ShowWayPoints); @@ -294,7 +295,7 @@ namespace Barotrauma else { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), TextManager.Get("Spawnpoint"), font: GUI.LargeFont); - + var spawnTypeContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), isHorizontal: true) { Stretch = true, @@ -318,7 +319,10 @@ namespace Barotrauma }; var descText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), - TextManager.Get("IDCardDescription"), font: GUI.SmallFont); + TextManager.Get("IDCardDescription"), font: GUI.SmallFont) + { + ToolTip = TextManager.Get("IDCardDescriptionTooltip") + }; GUITextBox propertyBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), descText.RectTransform, Anchor.CenterRight), IdCardDesc) { MaxTextLength = 150, @@ -342,7 +346,10 @@ namespace Barotrauma }; var idCardTagsText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), - TextManager.Get("IDCardTags"), font: GUI.SmallFont); + TextManager.Get("IDCardTags"), font: GUI.SmallFont) + { + ToolTip = TextManager.Get("IDCardTagsTooltip") + }; propertyBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), idCardTagsText.RectTransform, Anchor.CenterRight), string.Join(", ", idCardTags)) { MaxTextLength = 60, @@ -414,6 +421,6 @@ namespace Barotrauma PositionEditingHUD(); return editingHUD; - } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs index d76d85470..629752439 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs @@ -78,7 +78,7 @@ namespace Barotrauma.Networking txt = orderPrefab.GetChatMessage(orderMessageInfo.TargetCharacter?.Name, targetRoom, givingOrderToSelf: orderMessageInfo.TargetCharacter == senderCharacter, orderOption: orderOption, - priority: orderMessageInfo.Priority); + isNewOrder: orderMessageInfo.IsNewOrder); if (GameMain.Client.GameStarted && Screen.Selected == GameMain.GameScreen) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 1c78cf9bb..1c3b81c40 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -948,6 +948,9 @@ namespace Barotrauma.Networking case ServerPacketHeader.CREW: campaign?.ClientReadCrew(inc); break; + case ServerPacketHeader.MEDICAL: + campaign?.MedicalClinic?.ClientRead(inc); + break; case ServerPacketHeader.READY_CHECK: ReadyCheck.ClientRead(inc); break; @@ -1116,9 +1119,9 @@ namespace Barotrauma.Networking disconnectReason != DisconnectReason.InvalidVersion) { GameAnalyticsManager.AddErrorEventOnce( - "GameClient.HandleDisconnectMessage", - GameAnalyticsManager.ErrorSeverity.Debug, - "Client received a disconnect message. Reason: " + disconnectReason.ToString()); + "GameClient.HandleDisconnectMessage", + GameAnalyticsManager.ErrorSeverity.Debug, + "Client received a disconnect message. Reason: " + disconnectReason.ToString()); } if (disconnectReason == DisconnectReason.ServerFull) @@ -1271,7 +1274,15 @@ namespace Barotrauma.Networking private void ReadAchievement(IReadMessage inc) { string achievementIdentifier = inc.ReadString(); - SteamAchievementManager.UnlockAchievement(achievementIdentifier); + int amount = inc.ReadInt32(); + if (amount == 0) + { + SteamAchievementManager.UnlockAchievement(achievementIdentifier); + } + else + { + SteamAchievementManager.IncrementStat(achievementIdentifier, amount); + } } private void ReadTraitorMessage(IReadMessage inc) @@ -1471,7 +1482,7 @@ namespace Barotrauma.Networking serverSettings.LockAllDefaultWires = inc.ReadBoolean(); serverSettings.AllowRagdollButton = inc.ReadBoolean(); serverSettings.AllowLinkingWifiToChat = inc.ReadBoolean(); - GameMain.NetLobbyScreen.UsingShuttle = inc.ReadBoolean(); + bool usingShuttle = GameMain.NetLobbyScreen.UsingShuttle = inc.ReadBoolean(); GameMain.LightManager.LosMode = (LosMode)inc.ReadByte(); bool includesFinalize = inc.ReadBoolean(); inc.ReadPadBits(); GameMain.LightManager.LightingEnabled = true; @@ -1483,6 +1494,8 @@ namespace Barotrauma.Networking Task loadTask = null; var roundSummary = (GUIMessageBox.MessageBoxes.Find(c => c?.UserData is RoundSummary)?.UserData) as RoundSummary; + bool isOutpost = false; + if (gameMode != GameModePreset.MultiPlayerCampaign) { string levelSeed = inc.ReadString(); @@ -1621,6 +1634,7 @@ namespace Barotrauma.Networking { GameMain.GameSession.StartRound(levelData, mirrorLevel); } + isOutpost = levelData.Type == LevelData.LevelType.Outpost; } if (GameMain.Client?.ServerSettings?.Voting != null) @@ -1740,8 +1754,7 @@ namespace Barotrauma.Networking if (respawnAllowed) { - bool isOutpost = GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign && Level.Loaded?.Type == LevelData.LevelType.Outpost; - respawnManager = new RespawnManager(this, GameMain.NetLobbyScreen.UsingShuttle && !isOutpost ? GameMain.NetLobbyScreen.SelectedShuttle : null); + respawnManager = new RespawnManager(this, usingShuttle && !isOutpost ? GameMain.NetLobbyScreen.SelectedShuttle : null); } gameStarted = true; @@ -1872,7 +1885,7 @@ namespace Barotrauma.Networking if (int.TryParse(ownedIndexes[i], out int index)) { SubmarineInfo sub = GameMain.Client.ServerSubmarines[index]; - if (GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, "owned")) + if (GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, NetLobbyScreen.SubmarineDeliveryData.Owned)) { GameMain.GameSession.OwnedSubmarines.Add(sub); } @@ -1888,7 +1901,7 @@ namespace Barotrauma.Networking if (int.TryParse(ownedIndexes[i], out index)) { SubmarineInfo sub = GameMain.Client.ServerSubmarines[index]; - if (GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, "owned")) + if (GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, NetLobbyScreen.SubmarineDeliveryData.Owned)) { GameMain.NetLobbyScreen.ServerOwnedSubmarines.Add(sub); } @@ -2090,13 +2103,6 @@ namespace Barotrauma.Networking string selectShuttleName = inc.ReadString(); string selectShuttleHash = inc.ReadString(); - UInt16 campaignSubmarineIndexCount = inc.ReadUInt16(); - List campaignSubIndices = new List(); - for (int i = 0; i< campaignSubmarineIndexCount; i++) - { - campaignSubIndices.Add(inc.ReadUInt16()); - } - bool allowSubVoting = inc.ReadBoolean(); bool allowModeVoting = inc.ReadBoolean(); @@ -2157,16 +2163,11 @@ namespace Barotrauma.Networking if (GameMain.Client.IsServerOwner) RequestSelectMode(modeIndex); } - if (campaignSubIndices != null) + if (GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign) { - GameMain.NetLobbyScreen.CampaignSubmarines = new List(); - foreach (UInt16 campaignSubIndex in campaignSubIndices) + foreach (SubmarineInfo sub in ServerSubmarines.Where(s => !ServerSettings.HiddenSubs.Contains(s.Name))) { - SubmarineInfo sub = GameMain.Client.ServerSubmarines[campaignSubIndex]; - if (GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, "campaign")) - { - GameMain.NetLobbyScreen.CampaignSubmarines.Add(sub); - } + GameMain.NetLobbyScreen.CheckIfCampaignSubMatches(sub, NetLobbyScreen.SubmarineDeliveryData.Campaign); } } @@ -2599,7 +2600,6 @@ namespace Barotrauma.Networking NetLobbyScreen.FailedSubInfo failedCampaignSub = GameMain.NetLobbyScreen.FailedCampaignSubs.Find(s => s.Name == newSub.Name && s.Hash == newSub.MD5Hash.Hash); if (failedCampaignSub != default) { - GameMain.NetLobbyScreen.CampaignSubmarines.Add(newSub); GameMain.NetLobbyScreen.FailedCampaignSubs.Remove(failedCampaignSub); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs index 70e9b0728..4100bb358 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs @@ -100,7 +100,7 @@ namespace Barotrauma.Networking if (!isActive) { return; } if (steamId != hostSteamId) { return; } Close($"SteamP2P connection failed: {error}"); - OnDisconnectMessageReceived?.Invoke($"SteamP2P connection failed: {error}"); + OnDisconnectMessageReceived?.Invoke($"{DisconnectReason.SteamP2PError}/SteamP2P connection failed: {error}"); } private void OnP2PData(ulong steamId, byte[] data, int dataLength) @@ -167,14 +167,14 @@ namespace Barotrauma.Networking if (state == null) { Close("SteamP2P connection could not be established"); - OnDisconnectMessageReceived?.Invoke("SteamP2P connection could not be established"); + OnDisconnectMessageReceived?.Invoke(DisconnectReason.SteamP2PError.ToString()); } else { if (state?.P2PSessionError != Steamworks.P2PSessionError.None) { Close($"SteamP2P error code: {state?.P2PSessionError}"); - OnDisconnectMessageReceived?.Invoke($"SteamP2P error code: {state?.P2PSessionError}"); + OnDisconnectMessageReceived?.Invoke($"{DisconnectReason.SteamP2PError}/SteamP2P error code: {state?.P2PSessionError}"); } } connectionStatusTimer = 1.0f; @@ -210,7 +210,7 @@ namespace Barotrauma.Networking if (timeout < 0.0) { Close("Timed out"); - OnDisconnectMessageReceived?.Invoke(""); + OnDisconnectMessageReceived?.Invoke(DisconnectReason.SteamP2PTimeOut.ToString()); return; } @@ -349,13 +349,19 @@ namespace Barotrauma.Networking outMsg.Write((byte)PacketHeader.IsDisconnectMessage); outMsg.Write(msg ?? "Disconnected"); - Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable); - sentBytes += outMsg.LengthBytes; + try + { + Steamworks.SteamNetworking.SendP2PPacket(hostSteamId, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable); + sentBytes += outMsg.LengthBytes; + } + catch (Exception e) + { + DebugConsole.ThrowError("Failed to send a disconnect message to the server using SteamP2P.", e); + } Thread.Sleep(100); Steamworks.SteamNetworking.ResetActions(); - Steamworks.SteamNetworking.CloseP2PSessionWithUser(hostSteamId); steamAuthTicket?.Cancel(); steamAuthTicket = null; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs index 2b25e89a2..f0abd892a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerInfo.cs @@ -81,16 +81,15 @@ namespace Barotrauma.Networking public bool ContentPackagesMatch() { - var myContentPackages = ContentPackage.AllPackages; //make sure we have all the packages the server requires if (ContentPackageHashes.Count != ContentPackageWorkshopIds.Count) { return false; } for (int i = 0; i < ContentPackageWorkshopIds.Count; i++) { string hash = ContentPackageHashes[i]; UInt64 id = ContentPackageWorkshopIds[i]; - if (!myContentPackages.Any(myPackage => myPackage.MD5hash.Hash == hash)) + if (!GameMain.ServerListScreen.ContentPackagesByHash.ContainsKey(hash)) { - if (myContentPackages.Any(p => p.SteamWorkshopId == id)) { return false; } + if (GameMain.ServerListScreen.ContentPackagesByWorkshopId.ContainsKey(id)) { return false; } if (id == 0) { return false; } } } @@ -98,12 +97,6 @@ namespace Barotrauma.Networking return true; } - public bool ContentPackagesMatch(IEnumerable myContentPackageHashes) - { - HashSet contentPackageHashes = new HashSet(ContentPackageHashes); - return contentPackageHashes.SetEquals(myContentPackageHashes); - } - public void CreatePreviewWindow(GUIFrame frame) { if (frame == null) { return; } @@ -428,7 +421,7 @@ namespace Barotrauma.Networking return; } - var rules = ((Task>)t).Result; + t.TryGetResult(out Dictionary rules); SteamManager.AssignServerRulesToServerInfo(rules, this); onServerRulesReceived(this); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index d345d2a98..783d52326 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -94,10 +94,10 @@ namespace Barotrauma.Networking public void ClientAdminRead(IReadMessage incMsg) { - int count = incMsg.ReadUInt16(); - for (int i = 0; i < count; i++) + while (true) { UInt32 key = incMsg.ReadUInt32(); + if (key == 0) { break; } if (netProperties.ContainsKey(key)) { bool changedLocally = netProperties[key].ChangedLocally; @@ -153,8 +153,11 @@ namespace Barotrauma.Networking { ReadExtraCargo(incMsg); } - - ReadHiddenSubs(incMsg); + + if (requiredFlags.HasFlag(NetFlags.HiddenSubs)) + { + ReadHiddenSubs(incMsg); + } GameMain.NetLobbyScreen.UpdateSubVisibility(); bool isAdmin = incMsg.ReadBoolean(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs index 29295f520..a72a282f0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs @@ -143,8 +143,7 @@ namespace Barotrauma.Steam return; } - currentLobby = ((Task)lobby).Result; - + lobby.TryGetResult(out currentLobby); if (currentLobby == null) { DebugConsole.ThrowError("Failed to create Steam lobby: returned lobby was null"); @@ -250,7 +249,7 @@ namespace Barotrauma.Steam TaskPool.Add("JoinLobbyAsync", Steamworks.SteamMatchmaking.JoinLobbyAsync(lobbyID), (lobby) => { - currentLobby = ((Task)lobby).Result; + lobby.TryGetResult(out currentLobby); lobbyState = LobbyState.Joined; lobbyID = (currentLobby?.Id).Value; if (joinServer) @@ -293,10 +292,11 @@ namespace Barotrauma.Steam taskDone(); return; } - var lobbies = ((Task>)t).Result; - if (lobbies != null) + t.TryGetResult(out List lobbies); + IEnumerable lobbyAddCoroutine() { - foreach (var lobby in lobbies) + int i = 0; + foreach (var lobby in lobbies ?? Enumerable.Empty()) { if (string.IsNullOrEmpty(lobby.GetData("name"))) { continue; } @@ -312,9 +312,13 @@ namespace Barotrauma.Steam AssignLobbyDataToServerInfo(lobby, serverInfo); addToServerList(serverInfo); + i++; + if (i >= 16) { yield return CoroutineStatus.Running; i = 0; } } + taskDone(); + yield return CoroutineStatus.Success; } - taskDone(); + CoroutineManager.StartCoroutine(lobbyAddCoroutine()); }); Steamworks.ServerList.Internet serverQuery = new Steamworks.ServerList.Internet(); @@ -344,13 +348,10 @@ namespace Barotrauma.Steam return; } - var rules = ((Task>)t).Result; + t.TryGetResult(out Dictionary rules); AssignServerRulesToServerInfo(rules, serverInfo); - CrossThread.RequestExecutionOnMainThread(() => - { - addToServerList(serverInfo); - }); + addToServerList(serverInfo); }); } else @@ -618,7 +619,10 @@ namespace Barotrauma.Steam .WithLongDescription(); if (requireTags != null) { query = query.WithTags(requireTags); } - TaskPool.Add("GetSubscribedWorkshopItems", GetWorkshopItemsAsync(query), (task) => { onItemsFound?.Invoke(((Task>)task).Result); }); + TaskPool.Add("GetSubscribedWorkshopItems", GetWorkshopItemsAsync(query), (task) => + { + task.TryGetResult(out List result); onItemsFound?.Invoke(result); + }); } public static void GetPopularWorkshopItems(Action> onItemsFound, int amount, List requireTags = null) @@ -632,7 +636,7 @@ namespace Barotrauma.Steam TaskPool.Add("GetPopularWorkshopItems", GetWorkshopItemsAsync(query, amount, (item) => !item.IsSubscribed), (task) => { - var entries = ((Task>)task).Result; + task.TryGetResult(out List entries); //count the number of each unique tag foreach (var item in entries) @@ -677,7 +681,10 @@ namespace Barotrauma.Steam .WithLongDescription(); if (requireTags != null) query.WithTags(requireTags); - TaskPool.Add("GetPublishedWorkshopItems", GetWorkshopItemsAsync(query), (task) => { onItemsFound?.Invoke(((Task>)task).Result); }); + TaskPool.Add("GetPublishedWorkshopItems", GetWorkshopItemsAsync(query), (task) => + { + task.TryGetResult(out List result); onItemsFound?.Invoke(result); + }); } private static readonly HashSet pendingWorkshopSubscriptions = new HashSet(); @@ -724,7 +731,7 @@ namespace Barotrauma.Steam } else { - var item = ((Task)t).Result; + t.TryGetResult(out Steamworks.Ugc.Item? item); if (item != null) { if (item?.IsInstalled ?? false) @@ -1077,7 +1084,7 @@ namespace Barotrauma.Steam GameMain.SteamWorkshopScreen?.SetReinstallButtonStatus(item, true, GUI.Style.Red); return; } - string errorMsg = ((Task)task).Result; + task.TryGetResult(out string errorMsg); if (!string.IsNullOrWhiteSpace(errorMsg)) { DebugConsole.ThrowError($"Failed to copy \"{item.Title}\": {errorMsg}"); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs index afe8fb55a..d4c3b4793 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI/MultiPlayerCampaignSetupUI.cs @@ -246,6 +246,8 @@ namespace Barotrauma foreach (string saveFile in saveFiles) { + if (string.IsNullOrEmpty(saveFile)) { continue; } + string fileName = saveFile; string subName = ""; string saveTime = ""; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index 27efd044d..472063ab4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -45,6 +45,8 @@ namespace Barotrauma public UpgradeStore UpgradeStore { get; set; } + public MedicalClinicUI MedicalClinic { get; set; } + public CampaignUI(CampaignMode campaign, GUIComponent container) { Campaign = campaign; @@ -270,6 +272,9 @@ namespace Barotrauma // Submarine buying tab tabs[(int)CampaignMode.InteractionType.PurchaseSub] = new GUIFrame(new RectTransform(Vector2.One, container.RectTransform, Anchor.TopLeft), color: Color.Black * 0.9f); + tabs[(int)CampaignMode.InteractionType.MedicalClinic] = new GUIFrame(new RectTransform(Vector2.One, container.RectTransform), color: Color.Black * 0.9f); + MedicalClinic = new MedicalClinicUI(Campaign.MedicalClinic, GetTabContainer(CampaignMode.InteractionType.MedicalClinic)); + // mission info ------------------------------------------------------------------------- locationInfoPanel = new GUIFrame(new RectTransform(new Vector2(0.35f, 0.75f), GetTabContainer(CampaignMode.InteractionType.Map).RectTransform, Anchor.CenterRight) @@ -355,6 +360,10 @@ namespace Barotrauma case CampaignMode.InteractionType.Store: Store?.Update(deltaTime); break; + + case CampaignMode.InteractionType.MedicalClinic: + MedicalClinic?.Update(deltaTime); + break; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs index d85bf42ee..f54abb64b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EventEditor/EventEditorScreen.cs @@ -247,16 +247,6 @@ namespace Barotrauma return msgBox; } - private void NotifyPrompt(string header, string body) - { - GUIMessageBox msgBox = new GUIMessageBox(header, body, new[] { TextManager.Get("Ok") }, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175)); - msgBox.Buttons[0].OnClicked = delegate - { - msgBox.Close(); - return true; - }; - } - private bool SaveProjectToFile(GUIButton button, object o) { string directory = Path.GetFullPath("EventProjects"); @@ -315,7 +305,7 @@ namespace Barotrauma CreateNodes(prefab.ConfigElement, ref hadNodes); if (!hadNodes) { - NotifyPrompt(TextManager.Get("EventEditor.RandomGenerationHeader"), TextManager.Get("EventEditor.RandomGenerationBody")); + GUI.NotifyPrompt(TextManager.Get("EventEditor.RandomGenerationHeader"), TextManager.Get("EventEditor.RandomGenerationBody")); } return true; }); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 794643a21..e6318fc05 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -221,7 +221,6 @@ namespace Barotrauma public SubmarineInfo SelectedShuttle => ShuttleList.SelectedData as SubmarineInfo; public MultiPlayerCampaignSetupUI CampaignSetupUI; - public List CampaignSubmarines = new List(); // Passed onto the gamesession when created public List ServerOwnedSubmarines = new List(); @@ -611,6 +610,7 @@ namespace Barotrauma { OnClicked = (btn, obj) => { + if (GameMain.Client == null) { return true; } GameMain.Client.RequestStartRound(); CoroutineManager.StartCoroutine(WaitForStartRound(StartButton), "WaitForStartRound"); return true; @@ -628,7 +628,7 @@ namespace Barotrauma { OnSelected = (tickBox) => { - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, autoRestart: tickBox.Selected); + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, autoRestart: tickBox.Selected); return true; } }; @@ -655,6 +655,7 @@ namespace Barotrauma }; ServerName.OnDeselected += (textBox, key) => { + if (GameMain.Client == null) { return; } if (!textBox.Readonly) { GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Name); @@ -669,6 +670,7 @@ namespace Barotrauma ToolTip = TextManager.Get("addtofavorites"), OnSelected = (tickbox) => { + if (GameMain.Client == null) { return true; } ServerInfo info = GameMain.Client.ServerSettings.GetServerListInfo(); if (tickbox.Selected) { @@ -766,6 +768,7 @@ namespace Barotrauma }; ServerMessage.OnDeselected += (textBox, key) => { + if (GameMain.Client == null) { return; } if (!textBox.Readonly) { GameMain.Client?.ServerSettings?.ClientAdminWrite(ServerSettings.NetFlags.Message); @@ -849,7 +852,7 @@ namespace Barotrauma Selected = true, OnSelected = (GUITickBox box) => { - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, useRespawnShuttle: box.Selected); + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, useRespawnShuttle: box.Selected); return true; } }; @@ -868,7 +871,7 @@ namespace Barotrauma { OnSelected = (component, obj) => { - GameMain.Client.RequestSelectSub(component.Parent.GetChildIndex(component), isShuttle: true); + GameMain.Client?.RequestSelectSub(component.Parent.GetChildIndex(component), isShuttle: true); return true; } }; @@ -970,7 +973,7 @@ namespace Barotrauma { OnClicked = (_, __) => { - GameMain.Client.RequestSelectMode(ModeList.Content.GetChildIndex(ModeList.Content.GetChildByUserData(GameModePreset.Sandbox))); + GameMain.Client?.RequestSelectMode(ModeList.Content.GetChildIndex(ModeList.Content.GetChildByUserData(GameModePreset.Sandbox))); return true; } }; @@ -1026,7 +1029,7 @@ namespace Barotrauma { int missionTypeOr = tickbox.Selected ? (int)tickbox.UserData : (int)MissionType.None; int missionTypeAnd = (int)MissionType.All & (!tickbox.Selected ? (~(int)tickbox.UserData) : (int)MissionType.All); - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, (int)missionTypeOr, (int)missionTypeAnd); + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, (int)missionTypeOr, (int)missionTypeAnd); return true; } }; @@ -1059,7 +1062,7 @@ namespace Barotrauma SeedBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), seedLabel.RectTransform, Anchor.CenterRight)); SeedBox.OnDeselected += (textBox, key) => { - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.LevelSeed); + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.LevelSeed); }; clientDisabledElements.Add(SeedBox); LevelSeed = ToolBox.RandomSeed(8); @@ -1080,7 +1083,7 @@ namespace Barotrauma ToolTip = TextManager.Get("leveldifficultyexplanation"), OnReleased = (scrollbar, value) => { - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, levelDifficulty: scrollbar.BarScrollValue); + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, levelDifficulty: scrollbar.BarScrollValue); return true; } }; @@ -1112,8 +1115,7 @@ namespace Barotrauma { OnClicked = (button, obj) => { - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, traitorSetting: -1); - + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, traitorSetting: -1); return true; } }; @@ -1124,8 +1126,7 @@ namespace Barotrauma { OnClicked = (button, obj) => { - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, traitorSetting: 1); - + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, traitorSetting: 1); return true; } }; @@ -1143,7 +1144,7 @@ namespace Barotrauma { OnClicked = (button, obj) => { - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, botCount: -1); + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, botCount: -1); return true; } }; @@ -1153,7 +1154,7 @@ namespace Barotrauma { OnClicked = (button, obj) => { - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, botCount: 1); + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, botCount: 1); return true; } }; @@ -1169,7 +1170,7 @@ namespace Barotrauma { OnClicked = (button, obj) => { - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, botSpawnMode: -1); + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, botSpawnMode: -1); return true; } }; @@ -1179,7 +1180,7 @@ namespace Barotrauma { OnClicked = (button, obj) => { - GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, botSpawnMode: 1); + GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, botSpawnMode: 1); return true; } }; @@ -3576,7 +3577,13 @@ namespace Barotrauma return false; } - public bool CheckIfCampaignSubMatches(SubmarineInfo serverSubmarine, string deliveryData) + public enum SubmarineDeliveryData + { + Owned, + Campaign + } + + public bool CheckIfCampaignSubMatches(SubmarineInfo serverSubmarine, SubmarineDeliveryData deliveryData) { if (GameMain.Client == null) return false; @@ -3630,11 +3637,11 @@ namespace Barotrauma { FailedSubInfo fileInfo = (FailedSubInfo)userdata; - if (deliveryData == "owned") //owned!!!! + if (deliveryData == SubmarineDeliveryData.Owned) { FailedOwnedSubs.Add(fileInfo); } - else if (deliveryData == "campaign") + else if (deliveryData == SubmarineDeliveryData.Campaign) { FailedCampaignSubs.Add(fileInfo); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs index 9c004b7e3..461a8ba29 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs @@ -7,6 +7,7 @@ using Microsoft.Xna.Framework.Graphics; using RestSharp; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Net; using System.Net.NetworkInformation; @@ -21,6 +22,11 @@ namespace Barotrauma //how often the client is allowed to refresh servers private readonly TimeSpan AllowedRefreshInterval = new TimeSpan(0, 0, 3); + public ImmutableDictionary ContentPackagesByWorkshopId { get; private set; } + = ImmutableDictionary.Empty; + public ImmutableDictionary ContentPackagesByHash { get; private set; } + = ImmutableDictionary.Empty; + private GUIFrame menu; private GUIListBox serverList; @@ -1011,6 +1017,17 @@ namespace Barotrauma public override void Select() { base.Select(); + + ContentPackagesByWorkshopId = ContentPackage.AllPackages + .Select(p => new KeyValuePair(p.SteamWorkshopId, p)) + .Where(p => p.Key != 0) + .GroupBy(x => x.Key).Select(g => g.First()) + .ToImmutableDictionary(); + ContentPackagesByHash = ContentPackage.AllPackages + .Select(p => new KeyValuePair(p.MD5hash.Hash, p)) + .GroupBy(x => x.Key).Select(g => g.First()) + .ToImmutableDictionary(); + SelectedTab = ServerListTab.All; LoadServerFilters(GameMain.Config.ServerFilterElement); if (GameSettings.ShowOffensiveServerPrompt) @@ -1039,6 +1056,8 @@ namespace Barotrauma public override void Deselect() { + ContentPackagesByWorkshopId = ImmutableDictionary.Empty; + ContentPackagesByHash = ImmutableDictionary.Empty; base.Deselect(); GameMain.Config.SaveNewPlayerConfig(); @@ -1491,7 +1510,7 @@ namespace Barotrauma } TaskPool.Add($"Get{avatarSize}AvatarAsync", avatarFunc(friend.Id), (task) => { - Steamworks.Data.Image? img = ((Task)task).Result; + if (!task.TryGetResult(out Steamworks.Data.Image? img)) { return; } if (!img.HasValue) { return; } var avatarImage = img.Value; @@ -2203,7 +2222,7 @@ namespace Barotrauma TaskPool.PrintTaskExceptions(t, $"Failed to retrieve Workshop item info (ID {entry.Id})"); return; } - Steamworks.Ugc.Item? item = ((Task)t).Result; + t.TryGetResult(out Steamworks.Ugc.Item? item); if (!item.HasValue) { @@ -2313,7 +2332,7 @@ namespace Barotrauma { var info = obj.Item1; var text = obj.Item2; - info.Ping = ((Task)rtt).Result; info.PingChecked = true; + rtt.TryGetResult(out info.Ping); info.PingChecked = true; text.TextColor = GetPingTextColor(info.Ping); text.Text = info.Ping > -1 ? info.Ping.ToString() : "?"; lock (activePings) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs index 94064f687..60cdf08a0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs @@ -854,7 +854,7 @@ namespace Barotrauma (var it, var lb) = tuple; if (lb.Content.FindChild(item)?.GetChildByUserData("previewimage") is GUIImage previewImage) { - previewImage.Sprite = ((Task)task).Result; + if (task.TryGetResult(out Sprite sprite)) { previewImage.Sprite = sprite; } } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 1550a1afa..e90cfc275 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -99,6 +99,10 @@ namespace Barotrauma private GUIFrame previouslyUsedPanel; private GUIListBox previouslyUsedList; + private GUIButton visibilityButton; + private GUIFrame layerPanel; + private GUIListBox layerList; + private GUIFrame undoBufferPanel; private GUIFrame undoBufferDisclaimer; private GUIListBox undoBufferList; @@ -234,6 +238,8 @@ namespace Barotrauma public bool WiringMode => mode == Mode.Wiring; + public static readonly Dictionary Layers = new Dictionary(); + public SubEditorScreen() { cam = new Camera @@ -320,19 +326,34 @@ namespace Barotrauma new GUIFrame(new RectTransform(new Vector2(0.01f, 0.9f), paddedTopPanel.RectTransform), style: "VerticalLine"); - var visibilityButton = new GUIButton(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "SetupVisibilityButton") + visibilityButton = new GUIButton(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "SetupVisibilityButton") { ToolTip = TextManager.Get("SubEditorVisibilityButton") + '\n' + TextManager.Get("SubEditorVisibilityToolTip"), OnClicked = (btn, userData) => { previouslyUsedPanel.Visible = false; undoBufferPanel.Visible = false; + layerPanel.Visible = false; showEntitiesPanel.Visible = !showEntitiesPanel.Visible; showEntitiesPanel.RectTransform.AbsoluteOffset = new Point(Math.Max(Math.Max(btn.Rect.X, entityCountPanel.Rect.Right), saveAssemblyFrame.Rect.Right), TopPanel.Rect.Height); return true; } }; + new GUIButton(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "EditorLayerButton") + { + ToolTip = TextManager.Get("editor.layer.button") + '\n' + TextManager.Get("editor.layer.tooltip"), + OnClicked = (btn, userData) => + { + previouslyUsedPanel.Visible = false; + showEntitiesPanel.Visible = false; + undoBufferPanel.Visible = false; + layerPanel.Visible = !layerPanel.Visible; + layerPanel.RectTransform.AbsoluteOffset = new Point(Math.Max(Math.Max(btn.Rect.X, entityCountPanel.Rect.Right), saveAssemblyFrame.Rect.Right), TopPanel.Rect.Height); + return true; + } + }; + var previouslyUsedButton = new GUIButton(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "RecentlyUsedButton") { ToolTip = TextManager.Get("PreviouslyUsedLabel"), @@ -340,6 +361,7 @@ namespace Barotrauma { showEntitiesPanel.Visible = false; undoBufferPanel.Visible = false; + layerPanel.Visible = false; previouslyUsedPanel.Visible = !previouslyUsedPanel.Visible; previouslyUsedPanel.RectTransform.AbsoluteOffset = new Point(Math.Max(Math.Max(btn.Rect.X, entityCountPanel.Rect.Right), saveAssemblyFrame.Rect.Right), TopPanel.Rect.Height); return true; @@ -353,6 +375,7 @@ namespace Barotrauma { showEntitiesPanel.Visible = false; previouslyUsedPanel.Visible = false; + layerPanel.Visible = false; undoBufferPanel.Visible = !undoBufferPanel.Visible; undoBufferPanel.RectTransform.AbsoluteOffset = new Point(Math.Max(Math.Max(btn.Rect.X, entityCountPanel.Rect.Right), saveAssemblyFrame.Rect.Right), TopPanel.Rect.Height); return true; @@ -484,14 +507,81 @@ namespace Barotrauma //----------------------------------------------- + layerPanel = new GUIFrame(new RectTransform(new Vector2(0.175f, 0.4f), GUI.Canvas)) + { + Visible = false + }; + + GUILayoutGroup layerGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f), layerPanel.RectTransform, anchor: Anchor.Center)); + + layerList = new GUIListBox(new RectTransform(new Vector2(1f, 0.8f), layerGroup.RectTransform)) + { + ScrollBarVisible = true, + AutoHideScrollBar = false, + OnSelected = (component, o) => + { + if (!(o is string layer)) { return false; } + + MapEntity.SelectedList.Clear(); + foreach (MapEntity entity in MapEntity.mapEntityList.Where(me => !me.Removed && me.Layer == layer)) + { + if (entity.IsSelected) { continue; } + + MapEntity.SelectedList.Add(entity); + } + return true; + } + }; + + GUILayoutGroup layerButtonGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.2f), layerGroup.RectTransform)); + + GUILayoutGroup layerButtonTopGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), layerButtonGroup.RectTransform), isHorizontal: true); + + GUIButton layerAddButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), layerButtonTopGroup.RectTransform), text: TextManager.Get("editor.layer.newlayer"), style: "GUIButtonFreeScale") + { + OnClicked = (button, o) => + { + CreateNewLayer(null, MapEntity.SelectedList.ToList()); + return true; + } + }; + + GUIButton layerDeleteButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), layerButtonTopGroup.RectTransform), text: TextManager.Get("editor.layer.deletelayer"), style: "GUIButtonFreeScale") + { + OnClicked = (button, o) => + { + if (layerList.SelectedData is string layer) + { + RenameLayer(layer, null); + } + return true; + } + }; + + GUIButton layerRenameButton = new GUIButton(new RectTransform(new Vector2(1f, 0.5f), layerButtonGroup.RectTransform), text: TextManager.Get("editor.layer.renamelayer"), style: "GUIButtonFreeScale") + { + OnClicked = (button, o) => + { + if (layerList.SelectedData is string layer) + { + GUI.PromptTextInput(TextManager.Get("editor.layer.renamelayer"), layer, newName => + { + RenameLayer(layer, newName); + }); + } + + return true; + } + }; + + Vector2 subPanelSize = new Vector2(0.925f, 0.9f); + undoBufferPanel = new GUIFrame(new RectTransform(new Vector2(0.15f, 0.2f), GUI.Canvas) { MinSize = new Point(200, 200) }) { Visible = false }; - Vector2 undoSize = new Vector2(0.925f, 0.9f); - - undoBufferList = new GUIListBox(new RectTransform(undoSize, undoBufferPanel.RectTransform, Anchor.Center)) + undoBufferList = new GUIListBox(new RectTransform(subPanelSize, undoBufferPanel.RectTransform, Anchor.Center)) { ScrollBarVisible = true, OnSelected = (_, userData) => @@ -522,7 +612,7 @@ namespace Barotrauma } }; - undoBufferDisclaimer = new GUIFrame(new RectTransform(undoSize, undoBufferPanel.RectTransform, Anchor.Center), style: null) + undoBufferDisclaimer = new GUIFrame(new RectTransform(subPanelSize, undoBufferPanel.RectTransform, Anchor.Center), style: null) { Color = Color.Black, Visible = false @@ -1354,7 +1444,7 @@ namespace Barotrauma TimeSpan timeInEditor = DateTime.Now - editorSelectedTime; #if USE_STEAM - Steam.SteamManager.IncrementStat("hoursineditor", (float)timeInEditor.TotalHours); + SteamAchievementManager.IncrementStat("hoursineditor", (float)timeInEditor.TotalHours); #endif GUI.ForceMouseOn(null); @@ -1400,6 +1490,7 @@ namespace Barotrauma }); ClearFilter(); + ClearLayers(); } private void CreateDummyCharacter() @@ -1752,11 +1843,11 @@ namespace Barotrauma DebugConsole.ThrowError($"Saving the preview image of the submarine \"{Submarine.MainSub.Info.Name}\" failed.", e); savePreviewImage = false; } - Submarine.MainSub.SaveAs(savePath, savePreviewImage ? imgStream : null); + Submarine.MainSub.TrySaveAs(savePath, savePreviewImage ? imgStream : null); } else { - Submarine.MainSub.SaveAs(savePath); + Submarine.MainSub.TrySaveAs(savePath); } Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; @@ -2890,6 +2981,8 @@ namespace Barotrauma }; adjustLightsPrompt.Buttons[1].OnClicked += adjustLightsPrompt.Close; } + + ReconstructLayers(); } private void TryDeleteSub(SubmarineInfo sub) @@ -3075,6 +3168,8 @@ namespace Barotrauma if (container == null || container.DrawInventory) { target = item; } } + bool hasTargets = targets.Count > 0; + // Holding shift brings up special context menu options if (PlayerInput.IsShiftDown()) { @@ -3083,7 +3178,7 @@ namespace Barotrauma new ContextMenuOption("SubEditor.ToggleTransparency", isEnabled: true, onSelected: () => TransparentWiringMode = !TransparentWiringMode), new ContextMenuOption("SubEditor.ToggleGrid", isEnabled: true, onSelected: () => ShouldDrawGrid = !ShouldDrawGrid), new ContextMenuOption("SubEditor.PasteAssembly", isEnabled: true, () => PasteAssembly()), - new ContextMenuOption("Editor.SelectSame", isEnabled: targets.Count > 0, onSelected: delegate + new ContextMenuOption("Editor.SelectSame", isEnabled: hasTargets, onSelected: delegate { bool doorGapSelected = targets.Any(t => t is Gap gap && gap.ConnectedDoor != null); foreach (MapEntity match in MapEntity.mapEntityList.Where(e => e.prefab != null && targets.Any(t => t.prefab?.Identifier == e.prefab.Identifier) && !MapEntity.SelectedList.Contains(e))) @@ -3115,12 +3210,44 @@ namespace Barotrauma } else { + + List availableLayerOptions = new List + { + new ContextMenuOption("editor.layer.nolayer", true, onSelected: () => { MoveToLayer(null, targets); }) + }; + + availableLayerOptions.AddRange(Layers.Select(layer => new ContextMenuOption(layer.Key, true, onSelected: () => { MoveToLayer(layer.Key, targets); }))); + + ContextMenuOption[] layerOptions = + { + new ContextMenuOption("editor.layer.movetolayer", isEnabled: hasTargets, availableLayerOptions.ToArray()), + new ContextMenuOption("editor.layer.createlayer", isEnabled: hasTargets, onSelected: () => { CreateNewLayer(null, targets); }), + new ContextMenuOption("editor.layer.selectall", isEnabled: hasTargets, onSelected: () => + { + foreach (MapEntity match in MapEntity.mapEntityList.Where(e => targets.Any(t => !string.IsNullOrWhiteSpace(t.Layer) && t.Layer == e.Layer && !MapEntity.SelectedList.Contains(e)))) + { + if (MapEntity.SelectedList.Contains(match)) { continue; } + MapEntity.SelectedList.Add(match); + } + }), + new ContextMenuOption("editor.layer.openlayermenu", isEnabled: true, onSelected: () => + { + if (visibilityButton is null) { return; } + previouslyUsedPanel.Visible = false; + undoBufferPanel.Visible = false; + showEntitiesPanel.Visible = false; + layerPanel.Visible = !layerPanel.Visible; + layerPanel.RectTransform.AbsoluteOffset = new Point(Math.Max(Math.Max(visibilityButton.Rect.X, entityCountPanel.Rect.Right), saveAssemblyFrame.Rect.Right), TopPanel.Rect.Height); + }) + }; + GUIContextMenu.CreateContextMenu( - new ContextMenuOption("label.openlabel", isEnabled: target != null, onSelected: () => OpenItem(target)), - new ContextMenuOption("editor.cut", isEnabled: targets.Count > 0, onSelected: () => MapEntity.Cut(targets)), - new ContextMenuOption("editor.copytoclipboard", isEnabled: targets.Count > 0, onSelected: () => MapEntity.Copy(targets)), - new ContextMenuOption("editor.paste", isEnabled: MapEntity.CopiedList.Any(), onSelected: () => MapEntity.Paste(cam.ScreenToWorld(PlayerInput.MousePosition))), - new ContextMenuOption("delete", isEnabled: targets.Count > 0, onSelected: delegate + new ContextMenuOption("label.openlabel", isEnabled: target != null, onSelected: () => OpenItem(target)), + new ContextMenuOption("editor.layer", isEnabled: hasTargets, layerOptions), + new ContextMenuOption("editor.cut", isEnabled: hasTargets, onSelected: () => MapEntity.Cut(targets)), + new ContextMenuOption("editor.copytoclipboard", isEnabled: hasTargets, onSelected: () => MapEntity.Copy(targets)), + new ContextMenuOption("editor.paste", isEnabled: MapEntity.CopiedList.Any(), onSelected: () => MapEntity.Paste(cam.ScreenToWorld(PlayerInput.MousePosition))), + new ContextMenuOption("delete", isEnabled: hasTargets, onSelected: delegate { StoreCommand(new AddOrDeleteCommand(targets, true)); foreach (var me in targets) @@ -3131,6 +3258,76 @@ namespace Barotrauma } } + private void MoveToLayer(string layer, List content) + { + layer ??= string.Empty; + + foreach (MapEntity entity in content) + { + entity.Layer = layer; + } + } + + private void CreateNewLayer(string name, List content) + { + if (string.IsNullOrWhiteSpace(name)) + { + name = TextManager.Get("editor.layer.newlayer"); + } + + string incrementedName = name; + + for (int i = 1; Layers.ContainsKey(incrementedName); i++) + { + incrementedName = $"{name} ({i})"; + } + + name = incrementedName; + + if (content != null) + { + MoveToLayer(name, content); + } + + Layers.Add(name, true); + UpdateLayerPanel(); + } + + private void RenameLayer(string original, string newName) + { + Layers.Remove(original); + + foreach (MapEntity entity in MapEntity.mapEntityList.Where(entity => entity.Layer == original)) + { + entity.Layer = newName ?? string.Empty; + } + + if (!string.IsNullOrWhiteSpace(newName)) + { + Layers.TryAdd(newName, true); + } + UpdateLayerPanel(); + } + + private void ReconstructLayers() + { + ClearLayers(); + foreach (MapEntity entity in MapEntity.mapEntityList) + { + if (!string.IsNullOrWhiteSpace(entity.Layer)) + { + Layers.TryAdd(entity.Layer, true); + } + } + UpdateLayerPanel(); + } + + private void ClearLayers() + { + Layers.Clear(); + UpdateLayerPanel(); + } + private void PasteAssembly(string text = null, Vector2? pos = null) { pos ??= cam.ScreenToWorld(PlayerInput.MousePosition); @@ -4044,6 +4241,7 @@ namespace Barotrauma previouslyUsedPanel.AddToGUIUpdateList(); undoBufferPanel.AddToGUIUpdateList(); entityCountPanel.AddToGUIUpdateList(); + layerPanel.AddToGUIUpdateList(); TopPanel.AddToGUIUpdateList(); if (WiringMode) @@ -4147,9 +4345,55 @@ namespace Barotrauma GameMain.SubEditorScreen.UpdateUndoHistoryPanel(); } + private void UpdateLayerPanel() + { + if (layerPanel is null || layerList is null) { return; } + + layerList.Content.ClearChildren(); + + layerList.Deselect(); + + foreach (var (layer, isVisible) in Layers) + { + GUIFrame parent = new GUIFrame(new RectTransform(new Vector2(1f, 0.1f), layerList.Content.RectTransform), style: "ListBoxElement") + { + UserData = layer + }; + + GUILayoutGroup layerGroup = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); + + GUITickBox layerVisibleButton = new GUITickBox(new RectTransform(Vector2.One, layerGroup.RectTransform, scaleBasis: ScaleBasis.BothHeight), string.Empty) + { + Selected = isVisible, + OnSelected = box => + { + if (!Layers.TryGetValue(layer, out bool _)) + { + UpdateLayerPanel(); + return false; + } + + Layers[layer] = box.Selected; + return true; + } + }; + + layerGroup.Recalculate(); + + new GUITextBlock(new RectTransform(new Vector2(1.0f - layerVisibleButton.RectTransform.RelativeSize.X, 1f), layerGroup.RectTransform), layer, textAlignment: Alignment.CenterLeft) + { + CanBeFocused = false + }; + + layerGroup.Recalculate(); + } + + layerList.RecalculateChildren(); + } + public void UpdateUndoHistoryPanel() { - if (undoBufferPanel == null) { return; } + if (undoBufferPanel is null) { return; } undoBufferDisclaimer.Visible = mode == Mode.Wiring; @@ -4203,7 +4447,7 @@ namespace Barotrauma public override void Update(double deltaTime) { SkipInventorySlotUpdate = false; - ImageManager.Update((float) deltaTime); + ImageManager.Update((float)deltaTime); if (GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y) { @@ -4720,6 +4964,11 @@ namespace Barotrauma if (!saveAssemblyFrame.Rect.Contains(PlayerInput.MousePosition) && dummyCharacter?.SelectedConstruction == null && !WiringMode && GUI.MouseOn == null) { + if (layerList is { Visible: true } && GUI.KeyboardDispatcher.Subscriber == layerList) + { + GUI.KeyboardDispatcher.Subscriber = null; + } + MapEntity.UpdateSelecting(cam); } @@ -4998,7 +5247,7 @@ namespace Barotrauma var prevScissorRect = GameMain.Instance.GraphicsDevice.ScissorRectangle; - Rectangle subDimensions = Submarine.MainSub.CalculateDimensions(false); + Rectangle subDimensions = Submarine.MainSub.CalculateDimensions(onlyHulls: false); Vector2 viewPos = subDimensions.Center.ToVector2(); float scale = Math.Min(width / (float)subDimensions.Width, height / (float)subDimensions.Height); @@ -5087,5 +5336,19 @@ namespace Barotrauma public static bool IsSubEditor() => Screen.Selected is SubEditorScreen && !Submarine.Unloading; public static bool IsWiringMode() => Screen.Selected == GameMain.SubEditorScreen && GameMain.SubEditorScreen.WiringMode && !Submarine.Unloading; + public static bool IsLayerVisible(MapEntity entity) + { + if (!IsSubEditor()) { return true; } + + if (string.IsNullOrWhiteSpace(entity.Layer)) { return true; } + + if (!Layers.TryGetValue(entity.Layer, out bool isVisible)) + { + Layers.TryAdd(entity.Layer, true); + return true; + } + + return isVisible; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs index 38b34c0a0..55af5dcb5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs @@ -23,6 +23,7 @@ namespace Barotrauma private Submarine? submarine; private Character? dummyCharacter; public static Effect BlueprintEffect; + private GUIFrame container; private TabMenu tabMenu; @@ -42,21 +43,25 @@ namespace Barotrauma return true; } }; + } public override void Select() { base.Select(); - + container = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: "InnerGlow", color: Color.Black); + var tab = new GUIFrame(new RectTransform(Vector2.One, container.RectTransform), color: Color.Black * 0.9f); + MedicalClinicUI clinic = new MedicalClinicUI(new MedicalClinic(null!), tab); + clinic.RequestLatestPending(); if (dummyCharacter is { Removed: false }) { dummyCharacter?.Remove(); } - dummyCharacter = Character.Create(CharacterPrefab.HumanSpeciesName, Vector2.Zero, "", id: Entity.DummyID, hasAi: false); - dummyCharacter.Info.Job = new Job(JobPrefab.Prefabs.Where(jp => TalentTree.JobTalentTrees.ContainsKey(jp.Identifier)).GetRandom()); - dummyCharacter.Info.Name = "Galldren"; - dummyCharacter.Inventory.CreateSlots(); + // dummyCharacter = Character.Create(CharacterPrefab.HumanSpeciesName, Vector2.Zero, "", id: Entity.DummyID, hasAi: false); + // dummyCharacter.Info.Job = new Job(JobPrefab.Prefabs.Where(jp => TalentTree.JobTalentTrees.ContainsKey(jp.Identifier)).GetRandom()); + // dummyCharacter.Info.Name = "Galldren"; + // dummyCharacter.Inventory.CreateSlots(); Character.Controlled = dummyCharacter; GameMain.World.ProcessChanges(); @@ -67,9 +72,9 @@ namespace Barotrauma public override void AddToGUIUpdateList() { Frame.AddToGUIUpdateList(); - CharacterHUD.AddToGUIUpdateList(dummyCharacter); - dummyCharacter?.SelectedConstruction?.AddToGUIUpdateList(); - tabMenu.AddToGUIUpdateList(); + container.AddToGUIUpdateList(); + // CharacterHUD.AddToGUIUpdateList(dummyCharacter); + // dummyCharacter?.SelectedConstruction?.AddToGUIUpdateList(); } public override void Update(double deltaTime) @@ -92,12 +97,12 @@ namespace Barotrauma graphics.Clear(BackgroundColor); spriteBatch.Begin(SpriteSortMode.BackToFront, transformMatrix: Cam.Transform); - miniMapItem?.Draw(spriteBatch, false); - if (dummyCharacter is { } dummy) - { - dummyCharacter.DrawFront(spriteBatch, Cam); - dummyCharacter.Draw(spriteBatch, Cam); - } + // miniMapItem?.Draw(spriteBatch, false); + // if (dummyCharacter is { } dummy) + // { + // dummyCharacter.DrawFront(spriteBatch, Cam); + // dummyCharacter.Draw(spriteBatch, Cam); + // } spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index 66e9507f3..ab8ef0e8a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -567,8 +567,6 @@ namespace Barotrauma { if (SetPropertyValue(property, entity, numInput.FloatValue)) { - // This causes stack overflow. What's the purpose of it? - //numInput.FloatValue = (float)property.GetValue(entity); TrySendNetworkUpdate(entity, property); } }; @@ -674,7 +672,16 @@ namespace Barotrauma Text = value, OverflowClip = true }; - + + HashSet editedEntities = new HashSet(); + propertyBox.OnTextChanged += (textBox, text) => + { + foreach (var entity in MapEntity.SelectedList) + { + editedEntities.Add(entity); + } + return true; + }; propertyBox.OnDeselected += (textBox, keys) => OnApply(textBox); propertyBox.OnEnterPressed += (box, text) => OnApply(box); refresh += () => @@ -684,12 +691,25 @@ namespace Barotrauma bool OnApply(GUITextBox textBox) { + List prevSelected = MapEntity.SelectedList.ToList(); + //reselect the entities that were selected during editing + //otherwise multi-editing won't work when we deselect the entities with unapplied changes in the textbox + foreach (var entity in editedEntities) + { + MapEntity.SelectedList.Add(entity); + } if (SetPropertyValue(property, entity, textBox.Text)) { TrySendNetworkUpdate(entity, property); textBox.Text = (string) property.GetValue(entity); textBox.Flash(GUI.Style.Green, flashDuration: 1f); } + //restore the entities that were selected before applying + MapEntity.SelectedList.Clear(); + foreach (var entity in prevSelected) + { + MapEntity.SelectedList.Add(entity); + } return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs index 3d23f6590..2b06523f6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs @@ -14,9 +14,10 @@ namespace Barotrauma { private List particleEmitters; - private static HashSet ActiveLoopingSounds = new HashSet(); + private readonly static HashSet ActiveLoopingSounds = new HashSet(); private static double LastMuffleCheckTime; private readonly List sounds = new List(); + public IEnumerable Sounds { get { return sounds; } } private SoundSelectionMode soundSelectionMode; private SoundChannel soundChannel; private Entity soundEmitter; @@ -53,7 +54,7 @@ namespace Barotrauma } } - partial void ApplyProjSpecific(float deltaTime, Entity entity, IEnumerable targets, Hull hull, Vector2 worldPosition, bool playSound) + partial void ApplyProjSpecific(float deltaTime, Entity entity, IReadOnlyList targets, Hull hull, Vector2 worldPosition, bool playSound) { if (playSound) { @@ -84,7 +85,14 @@ namespace Barotrauma } else { - targetLimb = targets.FirstOrDefault(t => t is Limb) as Limb; + for (int i = 0; i < targets.Count; i++) + { + if (targets[i] is Limb limb) + { + targetLimb = limb; + break; + } + } } if (targetLimb != null && !targetLimb.Removed) { @@ -147,10 +155,6 @@ namespace Barotrauma GameAnalyticsManager.AddErrorEventOnce("StatusEffect.ApplyProjSpecific:SoundNull2" + Environment.StackTrace.CleanupStackTrace(), GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } - if (selectedSound.Sound.Disposed) - { - Submarine.ReloadRoundSound(selectedSound); - } soundChannel = SoundPlayer.PlaySound(selectedSound.Sound, worldPosition, selectedSound.Volume, selectedSound.Range, hullGuess: hull, ignoreMuffling: selectedSound.IgnoreMuffling); ignoreMuffling = selectedSound.IgnoreMuffling; if (soundChannel != null) { soundChannel.Looping = loopSound; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs index 3dade74cc..3ac7dd677 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/WikiImage.cs @@ -122,7 +122,7 @@ namespace Barotrauma { int width = 4096; int height = 4096; - Rectangle subDimensions = sub.CalculateDimensions(false); + Rectangle subDimensions = sub.Borders; Vector2 viewPos = subDimensions.Center.ToVector2(); float scale = Math.Min(width / (float)subDimensions.Width, height / (float)subDimensions.Height); diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 7bbfaee3a..444cd3bc8 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.15.23.0 + 0.16.0.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 315be0cad..a5084ad6e 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.15.23.0 + 0.16.0.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index f6d190782..98f23b7fa 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.15.23.0 + 0.16.0.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index f8be62734..0bb97a69c 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.15.23.0 + 0.16.0.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index ee4cfe02e..6f73823c1 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.15.23.0 + 0.16.0.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 8a6511484..35047f575 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs @@ -61,9 +61,9 @@ namespace Barotrauma GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.UpdateMoney }); } - partial void OnTalentGiven(string talentIdentifier) + partial void OnTalentGiven(TalentPrefab talentPrefab) { - GameServer.Log($"{GameServer.CharacterLogName(this)} has gained the talent '{talentIdentifier}'", ServerLog.MessageType.Talent); + GameServer.Log($"{GameServer.CharacterLogName(this)} has gained the talent '{talentPrefab.DisplayName}'", ServerLog.MessageType.Talent); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index a61d869c3..7eb20a6f5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -288,15 +288,18 @@ namespace Barotrauma { UInt32 talentIdentifier = msg.ReadUInt32(); var prefab = TalentPrefab.TalentPrefabs.Find(p => p.UIntIdentifier == talentIdentifier); - if (prefab != null) { talentSelection.Add(prefab.Identifier); } + if (prefab == null) { continue; } + + if (TalentTree.IsViableTalentForCharacter(this, prefab.Identifier, talentSelection)) + { + GiveTalent(prefab.Identifier); + talentSelection.Add(prefab.Identifier); + } } - talentSelection = TalentTree.CheckTalentSelection(this, talentSelection); - - foreach (string talent in talentSelection) + if (talentSelection.Count != talentCount) { - GiveTalent(talent); + DebugConsole.AddWarning($"Failed to unlock talents: the amount of unlocked talents doesn't match (client: {talentCount}, server: {talentSelection.Count})"); } - break; } break; diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index 9c08e13ab..63652614b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -17,6 +17,9 @@ namespace Barotrauma { public static readonly Version Version = Assembly.GetEntryAssembly().GetName().Version; + public static bool IsSingleplayer => NetworkMember == null; + public static bool IsMultiplayer => NetworkMember != null; + private static World world; public static World World diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs index 7f2316759..3dcc595ba 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/CrewManager.cs @@ -53,7 +53,7 @@ namespace Barotrauma foreach (var activeOrder in ActiveOrders) { if (!(activeOrder?.First is Order order) || activeOrder.Second.HasValue) { continue; } - OrderChatMessage.WriteOrder(msg, order, null, order.TargetSpatialEntity, null, 0, order.WallSectionIndex); + OrderChatMessage.WriteOrder(msg, order, targetCharacter: null, order.TargetSpatialEntity, orderOption: null, orderPriority: 0, order.WallSectionIndex, isNewOrder: true); bool hasOrderGiver = order.OrderGiver != null; msg.Write(hasOrderGiver); if (hasOrderGiver) diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 34883971f..65ff44e2c 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -348,7 +348,6 @@ namespace Barotrauma } } } - UpdateCampaignSubs(); SaveUtil.SaveGame(GameMain.GameSession.SavePath); PendingSubmarineSwitch = null; @@ -399,44 +398,12 @@ namespace Barotrauma Map.OnMissionsSelected += (loc, mission) => { LastUpdateID++; }; Reputation.OnAnyReputationValueChanged += () => { LastUpdateID++; }; - UpdateCampaignSubs(); - //increment save ID so clients know they're lacking the most up-to-date save file LastSaveID++; } - public static void UpdateCampaignSubs() - { - bool isSubmarineVisible(SubmarineInfo s) - => !GameMain.Server.ServerSettings.HiddenSubs.Any(h - => s.Name.Equals(h, StringComparison.OrdinalIgnoreCase)); - - List availableSubs = - SubmarineInfo.SavedSubmarines - .Where(s => - s.IsCampaignCompatible - && isSubmarineVisible(s)) - .ToList(); - - if (!availableSubs.Any()) - { - //None of the available subs were marked as campaign-compatible, just include all visible subs - availableSubs.AddRange( - SubmarineInfo.SavedSubmarines - .Where(isSubmarineVisible)); - } - - if (!availableSubs.Any()) - { - //No subs are visible at all! Just make the selected one available - availableSubs.Add(GameMain.NetLobbyScreen.SelectedSub); - } - - GameMain.NetLobbyScreen.CampaignSubmarines = availableSubs; - } - public bool CanPurchaseSub(SubmarineInfo info) - => info.Price <= Money && GameMain.NetLobbyScreen.CampaignSubmarines.Contains(info); + => info.Price <= Money && GetCampaignSubs().Contains(info); public void DiscardClientCharacterData(Client client) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs new file mode 100644 index 000000000..3e2afe845 --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs @@ -0,0 +1,191 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using Barotrauma.Networking; + +namespace Barotrauma +{ + internal partial class MedicalClinic + { + private enum RateLimitResult + { + OK, + LimitReached + } + + private struct RateLimitInfo + { + public int Requests; + public const int MaxRequests = 5; + public DateTimeOffset Expiry; + } + + private readonly Dictionary rateLimits = new Dictionary(); + + public void ServerRead(IReadMessage inc, Client sender) + { + NetworkHeader header = (NetworkHeader)inc.ReadByte(); + + switch (header) + { + case NetworkHeader.REQUEST_AFFLICTIONS: + ProcessRequestedAfflictions(inc, sender); + break; + case NetworkHeader.REQUEST_PENDING: + ProcessRequestedPending(sender); + break; + case NetworkHeader.ADD_PENDING: + ProcessNewAddition(inc, sender); + break; + case NetworkHeader.REMOVE_PENDING: + ProcessNewRemoval(inc, sender); + break; + case NetworkHeader.HEAL_PENDING: + ProcessHealing(sender); + break; + case NetworkHeader.CLEAR_PENDING: + ProcessClearing(sender); + break; + } + } + + private void ProcessNewAddition(IReadMessage inc, Client client) + { + if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + + NetCrewMember newCrewMember = INetSerializableStruct.Read(inc); + InsertPendingCrewMember(newCrewMember); + ServerSend(newCrewMember, NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable, reponseClient: client); + } + + private void ProcessNewRemoval(IReadMessage inc, Client client) + { + if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + + NetRemovedAffliction removed = INetSerializableStruct.Read(inc); + RemovePendingAffliction(removed.CrewMember, removed.Affliction); + ServerSend(removed, NetworkHeader.REMOVE_PENDING, DeliveryMethod.Reliable, reponseClient: client); + } + + private void ProcessRequestedPending(Client client) + { + if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + + INetSerializableStruct writeCrewMember = new NetPendingCrew + { + CrewMembers = PendingHeals.ToArray() + }; + + ServerSend(writeCrewMember, NetworkHeader.REQUEST_PENDING, DeliveryMethod.Reliable, targetClient: client); + } + + private void ProcessHealing(Client client) + { + if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + + HealRequestResult result = HealAllPending(); + ServerSend(new NetHealRequest { Result = result }, NetworkHeader.HEAL_PENDING, DeliveryMethod.Reliable, reponseClient: client); + } + + private void ProcessClearing(Client client) + { + if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + + if (!PendingHeals.Any()) { return; } + + ClearPendingHeals(); + ServerSend(null, NetworkHeader.CLEAR_PENDING, DeliveryMethod.Reliable, reponseClient: client); + } + + private void ProcessRequestedAfflictions(IReadMessage inc, Client client) + { + if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } + + NetCrewMember crewMember = INetSerializableStruct.Read(inc); + + CharacterInfo? foundInfo = crewMember.FindCharacterInfo(GetCrewCharacters()); + + NetAffliction[] pendingAfflictions = Array.Empty(); + int infoId = 0; + + if (foundInfo is { Character: { CharacterHealth: { } health } }) + { + pendingAfflictions = GetAllAfflictions(health); + infoId = foundInfo.GetIdentifierUsingOriginalName(); + } + + INetSerializableStruct writeCrewMember = new NetCrewMember + { + CharacterInfoID = infoId, + Afflictions = pendingAfflictions + }; + + ServerSend(writeCrewMember, NetworkHeader.REQUEST_AFFLICTIONS, DeliveryMethod.Unreliable, client); + } + + private RateLimitResult CheckRateLimit(Client client) + { + if (rateLimits.TryGetValue(client, out RateLimitInfo rateLimitInfo)) + { + if (rateLimitInfo.Expiry < DateTimeOffset.Now) + { + rateLimitInfo.Expiry = DateTimeOffset.Now.AddSeconds(5); + rateLimitInfo.Requests = 1; + } + else + { + if (rateLimitInfo.Requests > RateLimitInfo.MaxRequests) { return RateLimitResult.LimitReached; } + + rateLimitInfo.Requests++; + } + + rateLimits[client] = rateLimitInfo; + } + else + { + rateLimits.Add(client, new RateLimitInfo { Requests = 1, Expiry = DateTimeOffset.Now.AddSeconds(5) }); + } + + return RateLimitResult.OK; + } + + private IWriteMessage StartSending() + { + IWriteMessage msg = new WriteOnlyMessage(); + msg.Write((byte)ServerPacketHeader.MEDICAL); + return msg; + } + + private void ServerSend(INetSerializableStruct? netStruct, NetworkHeader header, DeliveryMethod deliveryMethod, Client? targetClient = null, Client? reponseClient = null) + { + if (targetClient is null) + { + foreach (Client c in GameMain.Server.ConnectedClients) + { + SendToClient(c); + } + + return; + } + + SendToClient(targetClient); + + void SendToClient(Client c) + { + MessageFlag flag = MessageFlag.Announce; + if (reponseClient != null && reponseClient == c) + { + flag = MessageFlag.Response; + } + + IWriteMessage msg = StartSending(); + msg.Write((byte)header); + msg.Write((byte)flag); + netStruct?.Write(msg); + GameMain.Server.ServerPeer.Send(msg, c.Connection, deliveryMethod); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs index 472cf14b9..c26c7cde2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Steering.cs @@ -50,14 +50,14 @@ namespace Barotrauma.Items.Components newSteeringInput = new Vector2(msg.ReadSingle(), msg.ReadSingle()); } - if (!item.CanClientAccess(c)) return; + if (!item.CanClientAccess(c)) { return; } user = c.Character; AutoPilot = autoPilot; if (dockingButtonClicked) { - item.SendSignal("1", "toggle_docking"); + item.SendSignal(new Signal("1", sender: c.Character), "toggle_docking"); GameMain.Server.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ComponentState, item.GetComponentIndex(this), true }); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs index 23c145bbf..5f9b0385b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs @@ -46,6 +46,7 @@ namespace Barotrauma.Items.Components msg.Write(DeteriorateAlways); msg.Write(tinkeringDuration); msg.Write(tinkeringStrength); + msg.Write(tinkeringPowersDevices); msg.Write(CurrentFixer == null ? (ushort)0 : CurrentFixer.ID); msg.WriteRangedInteger((int)currentFixerAction, 0, 2); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ButtonTerminal.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ButtonTerminal.cs index 056410165..4cdc0ec1b 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ButtonTerminal.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ButtonTerminal.cs @@ -8,7 +8,7 @@ namespace Barotrauma.Items.Components { int signalIndex = msg.ReadRangedInteger(0, Signals.Length - 1); if (!item.CanClientAccess(c)) { return; } - if (!SendSignal(signalIndex)) { return; } + if (!SendSignal(signalIndex, c.Character)) { return; } GameServer.Log($"{GameServer.CharacterLogName(c.Character)} sent a signal \"{Signals[signalIndex]}\" from {item.Name}", ServerLog.MessageType.ItemInteraction); item.CreateServerEvent(this, new object[] { signalIndex }); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 199860e32..34ae3de09 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -218,33 +218,6 @@ namespace Barotrauma.Networking if (shuttle != null) { GameMain.NetLobbyScreen.SelectedShuttle = shuttle; } } - List campaignSubs = new List(); - if (serverSettings.CampaignSubmarines != null && serverSettings.CampaignSubmarines.Length > 0) - { - string[] submarines = serverSettings.CampaignSubmarines.Split(ServerSettings.SubmarineSeparatorChar); - for (int i = 0; i < submarines.Length; i++) - { - SubmarineInfo subInfo = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == submarines[i]); - if (subInfo != null && subInfo.IsCampaignCompatible) - { - campaignSubs.Add(subInfo); - } - } - } - else - { - // Add vanilla submarines by default - for (int i = 0; i < SubmarineInfo.SavedSubmarines.Count(); i++) - { - SubmarineInfo subInfo = SubmarineInfo.SavedSubmarines.ElementAt(i); - if (subInfo.IsVanillaSubmarine() && subInfo.IsCampaignCompatible) - { - campaignSubs.Add(SubmarineInfo.SavedSubmarines.ElementAt(i)); - } - } - } - GameMain.NetLobbyScreen.CampaignSubmarines = campaignSubs; - started = true; GameAnalyticsManager.AddDesignEvent("GameServer:Start"); @@ -835,6 +808,9 @@ namespace Barotrauma.Networking case ClientPacketHeader.CREW: ReadCrewMessage(inc, connectedClient); break; + case ClientPacketHeader.MEDICAL: + ReadMedicalMessage(inc, connectedClient); + break; case ClientPacketHeader.READY_CHECK: ReadyCheck.ServerRead(inc, connectedClient); break; @@ -879,16 +855,16 @@ namespace Barotrauma.Networking } else if (entity is Character character) { - errorStr = "Missing character " + character.Name + " (event id " + eventID.ToString() + ", entity id " + entityID.ToString() + ")."; - errorStrNoName = "Missing character " + character.SpeciesName + "(event id " + eventID.ToString() + ", entity id " + entityID.ToString() + ")."; + errorStr = $"Missing character {character.Name} (event id {eventID}, entity id {entityID})."; + errorStrNoName = $"Missing character {character.SpeciesName} (event id {eventID}, entity id {entityID})."; } else if (entity is Item item) { - errorStr = errorStrNoName = "Missing item " + item.Name + " (event id " + eventID.ToString() + ", entity id " + entityID.ToString() + ")."; + errorStr = errorStrNoName = $"Missing item {item.Name}, sub: {item.Submarine?.Info?.Name ?? "none"} (event id {eventID}, entity id {entityID})."; } else { - errorStr = errorStrNoName = "Missing entity " + entity.ToString() + " (event id " + eventID.ToString() + ", entity id " + entityID.ToString() + ")."; + errorStr = errorStrNoName = $"Missing entity {entity}, sub: {entity.Submarine?.Info?.Name ?? "none"} (event id {eventID}, entity id {entityID})."; } break; } @@ -1234,6 +1210,14 @@ namespace Barotrauma.Networking } } + private void ReadMedicalMessage(IReadMessage inc, Client sender) + { + if (GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign) + { + mpCampaign.MedicalClinic.ServerRead(inc, sender); + } + } + private void ReadReadyToSpawnMessage(IReadMessage inc, Client sender) { sender.SpectateOnly = inc.ReadBoolean() && (serverSettings.AllowSpectating || sender.Connection == OwnerConnection); @@ -1861,24 +1845,6 @@ namespace Barotrauma.Networking outmsg.Write(GameMain.NetLobbyScreen.SelectedShuttle.Name); outmsg.Write(GameMain.NetLobbyScreen.SelectedShuttle.MD5Hash.ToString()); - List campaignSubIndices = new List(); - if (GameMain.NetLobbyScreen.SelectedMode == GameModePreset.MultiPlayerCampaign) - { - IReadOnlyList subList = GameMain.NetLobbyScreen.GetSubList(); - for (int i = 0; i < subList.Count; i++) - { - if (GameMain.NetLobbyScreen.CampaignSubmarines.Contains(subList[i])) - { - campaignSubIndices.Add(i); - } - } - } - outmsg.Write((UInt16)campaignSubIndices.Count); - foreach (int campaignSubIndex in campaignSubIndices) - { - outmsg.Write((UInt16)campaignSubIndex); - } - outmsg.Write(serverSettings.Voting.AllowSubVoting); outmsg.Write(serverSettings.Voting.AllowModeVoting); @@ -2166,7 +2132,6 @@ namespace Barotrauma.Networking else { SendStartMessage(roundStartSeed, GameMain.NetLobbyScreen.LevelSeed, GameMain.GameSession, connectedClients, false); - GameMain.GameSession.StartRound(GameMain.NetLobbyScreen.LevelSeed, serverSettings.SelectedLevelDifficulty); Log("Game mode: " + selectedMode.Name, ServerLog.MessageType.ServerMessage); Log("Submarine: " + selectedSub.Name, ServerLog.MessageType.ServerMessage); @@ -2186,8 +2151,7 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Failure; } - MissionMode missionMode = GameMain.GameSession.GameMode as MissionMode; - bool missionAllowRespawn = missionMode == null || !missionMode.Missions.Any(m => !m.AllowRespawn); + bool missionAllowRespawn = !(GameMain.GameSession.GameMode is MissionMode missionMode) || !missionMode.Missions.Any(m => !m.AllowRespawn); bool isOutpost = campaign != null && campaign.NextLevel?.Type == LevelData.LevelType.Outpost; if (serverSettings.AllowRespawn && missionAllowRespawn) @@ -2263,7 +2227,7 @@ namespace Barotrauma.Networking characterInfos.Add(client.CharacterInfo); if (client.CharacterInfo.Job == null || client.CharacterInfo.Job.Prefab != client.AssignedJob.First) { - client.CharacterInfo.Job = new Job(client.AssignedJob.First, client.AssignedJob.Second); + client.CharacterInfo.Job = new Job(client.AssignedJob.First, Rand.RandSync.Unsynced, client.AssignedJob.Second); } } @@ -2475,7 +2439,7 @@ namespace Barotrauma.Networking msg.Write(serverSettings.LockAllDefaultWires); msg.Write(serverSettings.AllowRagdollButton); msg.Write(serverSettings.AllowLinkingWifiToChat); - msg.Write(serverSettings.UseRespawnShuttle); + msg.Write(serverSettings.UseRespawnShuttle || (gameStarted && respawnManager.UsingShuttle)); msg.Write((byte)serverSettings.LosMode); msg.Write(includesFinalize); msg.WritePadBits(); @@ -3372,14 +3336,40 @@ namespace Barotrauma.Networking } } + public void IncrementStat(Character character, string achievementIdentifier, int amount) + { + achievementIdentifier = achievementIdentifier.ToLowerInvariant(); + foreach (Client client in connectedClients) + { + if (client.Character == character) + { + IncrementStat(client, achievementIdentifier, amount); + return; + } + } + } + public void GiveAchievement(Client client, string achievementIdentifier) { - if (client.GivenAchievements.Contains(achievementIdentifier)) return; + if (client.GivenAchievements.Contains(achievementIdentifier)) { return; } client.GivenAchievements.Add(achievementIdentifier); IWriteMessage msg = new WriteOnlyMessage(); msg.Write((byte)ServerPacketHeader.ACHIEVEMENT); msg.Write(achievementIdentifier); + msg.Write(0); + + serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); + } + + public void IncrementStat(Client client, string achievementIdentifier, int amount) + { + if (client.GivenAchievements.Contains(achievementIdentifier)) { return; } + + IWriteMessage msg = new WriteOnlyMessage(); + msg.Write((byte)ServerPacketHeader.ACHIEVEMENT); + msg.Write(achievementIdentifier); + msg.Write(amount); serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable); } @@ -3733,7 +3723,7 @@ namespace Barotrauma.Networking if (assignedPlayerCount[jobPrefab] >= jobPrefab.MaxNumber) { continue; } var variant = Rand.Range(0, jobPrefab.Variants, Rand.RandSync.Server); - unassignedBots[0].Job = new Job(jobPrefab, variant); + unassignedBots[0].Job = new Job(jobPrefab, Rand.RandSync.Server, variant); assignedPlayerCount[jobPrefab]++; unassignedBots.Remove(unassignedBots[0]); canAssign = true; @@ -3756,7 +3746,7 @@ namespace Barotrauma.Networking { var job = remainingJobs.GetRandom(); var variant = Rand.Range(0, job.Variants); - c.Job = new Job(job, variant); + c.Job = new Job(job, Rand.RandSync.Unsynced, variant); assignedPlayerCount[c.Job.Prefab]++; } } @@ -3842,16 +3832,6 @@ namespace Barotrauma.Networking if (GameMain.NetLobbyScreen.SelectedSub != null) { serverSettings.SelectedSubmarine = GameMain.NetLobbyScreen.SelectedSub.Name; } if (GameMain.NetLobbyScreen.SelectedShuttle != null) { serverSettings.SelectedShuttle = GameMain.NetLobbyScreen.SelectedShuttle.Name; } - if (GameMain.NetLobbyScreen.CampaignSubmarines != null) - { - string submarinesString = string.Empty; - for (int i = 0; i < GameMain.NetLobbyScreen.CampaignSubmarines.Count; i++) - { - submarinesString += GameMain.NetLobbyScreen.CampaignSubmarines[i].Name + ServerSettings.SubmarineSeparatorChar; - } - submarinesString.Trim(ServerSettings.SubmarineSeparatorChar); - serverSettings.CampaignSubmarines = submarinesString; - } serverSettings.SaveSettings(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index f39ffd210..604c897f3 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -351,7 +351,7 @@ namespace Barotrauma.Networking { if (campaign?.GetClientCharacterData(c) == null || c.CharacterInfo.Job == null) { - c.CharacterInfo.Job = new Job(c.AssignedJob.First, c.AssignedJob.Second); + c.CharacterInfo.Job = new Job(c.AssignedJob.First, Rand.RandSync.Unsynced, c.AssignedJob.Second); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index cb0a2582f..439891f62 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -11,6 +11,21 @@ namespace Barotrauma.Networking { partial class ServerSettings { + partial class NetPropertyData + { + private object lastSyncedValue; + public UInt16 LastUpdateID { get; private set; } + + public void SyncValue() + { + if (!PropEquals(lastSyncedValue, Value)) + { + LastUpdateID = (UInt16)(GameMain.NetLobbyScreen.LastUpdateID); + lastSyncedValue = Value; + } + } + } + public static readonly string ClientPermissionsFile = "Data" + Path.DirectorySeparatorChar + "clientpermissions.xml"; public static readonly char SubmarineSeparatorChar = '|'; @@ -35,20 +50,25 @@ namespace Barotrauma.Networking LoadClientPermissions(); } - private void WriteNetProperties(IWriteMessage outMsg) + private void WriteNetProperties(IWriteMessage outMsg, Client c) { - outMsg.Write((UInt16)netProperties.Keys.Count); foreach (UInt32 key in netProperties.Keys) { - outMsg.Write(key); - netProperties[key].Write(outMsg); + var property = netProperties[key]; + property.SyncValue(); + if (property.LastUpdateID > c.LastRecvLobbyUpdate) + { + outMsg.Write(key); + netProperties[key].Write(outMsg); + } } + outMsg.Write((UInt32)0); } public void ServerAdminWrite(IWriteMessage outMsg, Client c) { c.LastSentServerSettingsUpdate = LastPropertyUpdateId; - WriteNetProperties(outMsg); + WriteNetProperties(outMsg, c); WriteMonsterEnabled(outMsg); BanList.ServerAdminWrite(outMsg, c); Whitelist.ServerAdminWrite(outMsg, c); @@ -79,8 +99,11 @@ namespace Barotrauma.Networking { WriteExtraCargo(outMsg); } - - WriteHiddenSubs(outMsg); + + if (requiredFlags.HasFlag(NetFlags.HiddenSubs)) + { + WriteHiddenSubs(outMsg); + } if (c.HasPermission(Networking.ClientPermissions.ManageSettings) && !NetIdUtils.IdMoreRecentOrMatches(c.LastRecvServerSettingsUpdate, LastPropertyUpdateId)) @@ -164,6 +187,7 @@ namespace Barotrauma.Networking { ReadHiddenSubs(incMsg); changed |= true; + UpdateFlag(NetFlags.HiddenSubs); } if (flags.HasFlag(NetFlags.Misc)) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs index 69a37fa1f..cc3baa6d0 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs @@ -32,26 +32,6 @@ namespace Barotrauma set { selectedShuttle = value; lastUpdateID++; } } - [Obsolete("TODO: this list shouldn't exist, the client should just use the visible subs list instead")] - public List CampaignSubmarines - { - get - { - return campaignSubmarines; - } - set - { - campaignSubmarines = value; - lastUpdateID++; - if (GameMain.NetworkMember?.ServerSettings != null) - { - GameMain.NetworkMember.ServerSettings.ServerDetailsChanged = true; - } - } - } - - private List campaignSubmarines; - public GameModePreset[] GameModes { get; } private int selectedModeIndex; diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index ec80b7889..3dfa62e6d 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.15.23.0 + 0.16.0.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 4a2e502cc..8f16f561e 100644 --- a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml +++ b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml @@ -144,6 +144,7 @@ + diff --git a/Barotrauma/BarotraumaShared/README.txt b/Barotrauma/BarotraumaShared/README.txt index a71341163..fe1f66f2a 100644 --- a/Barotrauma/BarotraumaShared/README.txt +++ b/Barotrauma/BarotraumaShared/README.txt @@ -2,8 +2,8 @@ http://www.barotraumagame.com -© 2018-2020 FakeFish Ltd. All rights reserved. -© 2019-2020 Daedalic Entertainment GmbH. The Daedalic logo is a trademark of Daedalic Entertainment GmbH, Germany. All rights reserved. +© 2018-2022 FakeFish Ltd. All rights reserved. +© 2019-2022 Daedalic Entertainment GmbH. The Daedalic logo is a trademark of Daedalic Entertainment GmbH, Germany. All rights reserved. Privacy policy: http://privacypolicy.daedalic.com See the wiki for more detailed info and instructions: diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 0500a03c8..64c069f5f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -150,7 +150,7 @@ namespace Barotrauma private CoroutineHandle disableTailCoroutine; - private readonly IEnumerable myBodies; + private readonly List myBodies; public LatchOntoAI LatchOntoAI { get; private set; } public SwarmBehavior SwarmBehavior { get; private set; } @@ -306,7 +306,8 @@ namespace Barotrauma requiredHoleCount = (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderWidth) / Structure.WallSectionSize); - myBodies = Character.AnimController.Limbs.Select(l => l.body.FarseerBody); + myBodies = Character.AnimController.Limbs.Select(l => l.body.FarseerBody).ToList(); + myBodies.Add(Character.AnimController.Collider.FarseerBody); } private CharacterParams.AIParams _aiParams; @@ -1837,7 +1838,7 @@ namespace Barotrauma if (!attack.IsValidTarget(target)) { return false; } if (target is ISerializableEntity se && target is Character) { - if (attack.Conditionals.Any(c => !c.Matches(se))) { return false; } + if (attack.Conditionals.Any(c => !c.TargetSelf && !c.Matches(se))) { return false; } } if (attack.Conditionals.Any(c => c.TargetSelf && !c.Matches(Character))) { return false; } if (attack.Ranged) @@ -2182,10 +2183,22 @@ namespace Barotrauma float margin = MathHelper.PiOver4 * distanceFactor; if (angle < margin) { - var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel; - var pickedBody = Submarine.PickBody(weapon.SimPosition, target.SimPosition, myBodies, collisionCategories, allowInsideFixture: true); + var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel; + var pickedBody = Submarine.PickBody(weapon.SimPosition, Character.GetRelativeSimPosition(target), myBodies, collisionCategories, allowInsideFixture: true); if (pickedBody != null) { + if (target is MapEntity) + { + if (pickedBody.UserData is Submarine sub && sub == target.Submarine) + { + return true; + } + else if (target == pickedBody.UserData) + { + return true; + } + } + Character t = null; if (pickedBody.UserData is Character c) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index f85b5f9dd..c9e8d616d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -832,6 +832,8 @@ namespace Barotrauma if (container == null) { return 0; } if (!container.HasAccess(character)) { return 0; } if (!container.Inventory.CanBePut(containableItem)) { return 0; } + var rootContainer = container.Item.GetRootContainer(); + if (rootContainer?.GetComponent() != null || rootContainer?.GetComponent() != null) { return 0; } if (container.ShouldBeContained(containableItem, out bool isRestrictionsDefined)) { if (isRestrictionsDefined) @@ -1088,6 +1090,7 @@ namespace Barotrauma private void RespondToAttack(Character attacker, AttackResult attackResult) { + float minorDamageThreshold = 10; float healAmount = 0.0f; if (attacker != null) { @@ -1135,6 +1138,7 @@ namespace Barotrauma // Don't react to attackers that are outside of the sub (e.g. AoE attacks) return; } + bool isAttackerInfected = false; bool isAttackerFightingEnemy = false; if (IsFriendly(attacker)) { @@ -1155,10 +1159,14 @@ namespace Barotrauma } else { + isAttackerInfected = attacker.CharacterHealth.GetAfflictionStrength("alieninfection") > 0; // Inform other NPCs - if (cumulativeDamage > 1 || totalDamage >= 10) + if (isAttackerInfected || cumulativeDamage > 1 || totalDamage >= minorDamageThreshold) { - InformOtherNPCs(cumulativeDamage); + if (GameMain.IsMultiplayer || !attacker.IsPlayer || Character.TeamID != attacker.TeamID) + { + InformOtherNPCs(cumulativeDamage); + } } if (Character.IsBot) { @@ -1167,7 +1175,7 @@ namespace Barotrauma { if (Character.IsSecurity) { - if (attacker.TeamID != Character.TeamID && cumulativeDamage > 1 || cumulativeDamage > 10) + if (attacker.TeamID != Character.TeamID && cumulativeDamage > 1 || cumulativeDamage > minorDamageThreshold) { Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityarrest"), null, 0.50f, "attackedbyfriendlysecurityarrest", minDurationBetweenSimilar: 30.0f); } @@ -1181,26 +1189,8 @@ namespace Barotrauma Character.Speak(TextManager.Get("DialogAttackedByFriendly"), null, 0.50f, "attackedbyfriendly", minDurationBetweenSimilar: 30.0f); } } - if (cumulativeDamage > 1 && attacker.TeamID != Character.TeamID) - { - // If the attacker is using a low damage and high frequency weapon like a repair tool, we shouldn't use any delay. - AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage), attacker, delay: realDamage > 1 ? GetReactionTime() : 0); - } - else - { - // Don't react to minor (accidental) dmg done by characters that are in the same team - if (cumulativeDamage < 10) - { - if (!Character.IsSecurity && cumulativeDamage > 1) - { - AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker); - } - } - else - { - AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage, dmgThreshold: 50), attacker, GetReactionTime() * 2); - } - } + // If the attacker is using a low damage and high frequency weapon like a repair tool, we shouldn't use any delay. + AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage), attacker, delay: realDamage > 1 ? GetReactionTime() : 0); } if (!isAttackerFightingEnemy) { @@ -1242,13 +1232,13 @@ namespace Barotrauma continue; } } - var combatMode = DetermineCombatMode(otherCharacter, cumulativeDamage, isWitnessing, dmgThreshold: attacker.TeamID == Character.TeamID ? 50 : 10); + var combatMode = DetermineCombatMode(otherCharacter, cumulativeDamage, isWitnessing); float delay = isWitnessing ? GetReactionTime() : Rand.Range(2.0f, 5.0f, Rand.RandSync.Unsynced); otherHumanAI.AddCombatObjective(combatMode, attacker, delay); } } - AIObjectiveCombat.CombatMode DetermineCombatMode(Character c, float cumulativeDamage, bool isWitnessing = false, float dmgThreshold = 10, bool allowOffensive = true) + AIObjectiveCombat.CombatMode DetermineCombatMode(Character c, float cumulativeDamage, bool isWitnessing = false) { if (!IsFriendly(attacker)) { @@ -1268,6 +1258,17 @@ namespace Barotrauma } else { + float dmgThreshold = attacker.TeamID == Character.TeamID ? 50 : minorDamageThreshold; + if (isAttackerInfected) + { + cumulativeDamage = 100; + } + if (GameMain.IsSingleplayer && attacker.IsPlayer && Character.TeamID == attacker.TeamID) + { + // Bots in the player team never act aggressively in single player when attacked by the player + dmgThreshold = minorDamageThreshold; + return cumulativeDamage > dmgThreshold ? AIObjectiveCombat.CombatMode.Retreat : AIObjectiveCombat.CombatMode.None; + } if (Character.Submarine == null || !Character.Submarine.GetConnectedSubs().Contains(attacker.Submarine)) { // Outside or attacked from an unconnected submarine -> don't react. @@ -1279,17 +1280,17 @@ namespace Barotrauma isAttackerFightingEnemy = true; return AIObjectiveCombat.CombatMode.None; } - else if (isWitnessing && Character.CombatAction != null && !c.IsSecurity) + if (isWitnessing && Character.CombatAction != null && !c.IsSecurity) { return Character.CombatAction.WitnessReaction; } - else if (attacker.IsPlayer && FindInstigator() is Character instigator) + if (attacker.IsPlayer && FindInstigator() is Character instigator) { - // The guards don't react when the player there's an instigator around + // The guards don't react to player's aggressions when there's an instigator around isAttackerFightingEnemy = true; return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : (instigator.CombatAction != null ? instigator.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat); } - else if (attacker.TeamID == CharacterTeamType.FriendlyNPC && !(attacker.AIController.IsMentallyUnstable || attacker.AIController.IsMentallyUnstable)) + if (attacker.TeamID == CharacterTeamType.FriendlyNPC && !(attacker.AIController.IsMentallyUnstable || attacker.AIController.IsMentallyUnstable)) { if (c.IsSecurity) { @@ -1307,15 +1308,11 @@ namespace Barotrauma // Already targeting the attacker -> treat as a more serious threat. cumulativeDamage *= 2; } - if (attackResult.Afflictions != null && attackResult.Afflictions.Any(a => a is AfflictionHusk)) - { - cumulativeDamage = 100; - } if (cumulativeDamage > dmgThreshold) { if (c.IsSecurity) { - return c.IsSecurity && allowOffensive ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Arrest; + return c.IsSecurity ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Arrest; } else { @@ -1838,7 +1835,7 @@ namespace Barotrauma bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective(); bool ignoreWater = HasDivingSuit(character); bool ignoreOxygen = ignoreWater || HasDivingMask(character); - bool ignoreEnemies = ObjectiveManager.IsCurrentOrder() || ObjectiveManager.Objectives.Any(o => o is AIObjectiveFightIntruders); + bool ignoreEnemies = ObjectiveManager.IsCurrentOrder() || ObjectiveManager.GetActiveObjectives().Any(); float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies); if (isCurrentHull) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs index ca242f790..e5a71a3ed 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs @@ -51,19 +51,13 @@ namespace Barotrauma private set; } - /// - /// Returns true if the current or the next node is in ladders. - /// - public bool InLadders => - currentPath != null && currentPath.CurrentNode != null && - (currentPath.CurrentNode.Ladders != null && currentPath.CurrentNode.Ladders.Item.IsInteractable(character) || - (currentPath.NextNode != null && currentPath.NextNode.Ladders != null && currentPath.NextNode.Ladders.Item.IsInteractable(character))); - /// /// Returns true if any node in the path is in stairs /// public bool InStairs => currentPath != null && currentPath.Nodes.Any(n => n.Stairs != null); + public bool IsCurrentNodeLadder => currentPath?.CurrentNode?.Ladders != null && currentPath.CurrentNode.Ladders.Item.IsInteractable(character); + public bool IsNextNodeLadder => GetNextLadder() != null; public bool IsNextLadderSameAsCurrent @@ -99,14 +93,24 @@ namespace Barotrauma base.Update(speed); float step = 1.0f / 60.0f; checkDoorsTimer -= step; - buttonPressTimer -= step; + if (lastDoor.door == null || !lastDoor.shouldBeOpen || lastDoor.door.IsOpen) + { + buttonPressTimer = 0; + } + else + { + buttonPressTimer -= step; + } findPathTimer -= step; } public void SetPath(SteeringPath path) { currentPath = path; - if (path.Nodes.Any()) currentTarget = path.Nodes[path.Nodes.Count - 1].SimPosition; + if (path.Nodes.Any()) + { + currentTarget = path.Nodes[path.Nodes.Count - 1].SimPosition; + } findPathTimer = Math.Min(findPathTimer, 1.0f); IsPathDirty = false; } @@ -124,15 +128,9 @@ namespace Barotrauma public void SteeringSeek(Vector2 target, float weight, float minGapWidth = 0, Func startNodeFilter = null, Func endNodeFilter = null, Func nodeFilter = null, bool checkVisiblity = true) { - if (buttonPressTimer > 0 && lastDoor.door != null && lastDoor.state && !lastDoor.door.IsOpen) - { - // We have pressed the button and are waiting for the door to open -> Hold still until we can press the button again. - Reset(); - } - else - { - steering += CalculateSteeringSeek(target, weight, minGapWidth, startNodeFilter, endNodeFilter, nodeFilter, checkVisiblity); - } + // Have to use a variable here or resetting doesn't work. + Vector2 addition = CalculateSteeringSeek(target, weight, minGapWidth, startNodeFilter, endNodeFilter, nodeFilter, checkVisiblity); + steering += addition; } /// @@ -328,7 +326,13 @@ namespace Barotrauma { CheckDoorsInPath(); doorsChecked = true; - } + } + if (buttonPressTimer > 0 && lastDoor.door != null && lastDoor.shouldBeOpen && !lastDoor.door.IsOpen) + { + // We have pressed the button and are waiting for the door to open -> Hold still until we can press the button again. + Reset(); + return Vector2.Zero; + } Vector2 pos = host.WorldPosition; bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater; // Only humanoids can climb ladders @@ -378,7 +382,7 @@ namespace Barotrauma //at the same height as the waypoint if (Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y) < (collider.height / 2 + collider.radius) * 1.25f) { - float heightFromFloor = character.AnimController.GetColliderBottom().Y - character.AnimController.FloorY; + float heightFromFloor = character.AnimController.GetHeightFromFloor(); if (heightFromFloor <= 0.0f) { diff.Y = Math.Max(diff.Y, 100); @@ -516,7 +520,7 @@ namespace Barotrauma return ConvertUnits.ToDisplayUnits(Math.Max(colliderSize.X, colliderSize.Y)); } - private (Door door, bool state) lastDoor; + private (Door door, bool shouldBeOpen) lastDoor; private float GetDoorCheckTime() { if (steering.LengthSquared() > 0) @@ -539,7 +543,6 @@ namespace Barotrauma WayPoint nextWaypoint = null; Door door = null; bool shouldBeOpen = false; - if (currentPath.Nodes.Count == 1) { door = currentPath.Nodes.First().ConnectedDoor; @@ -645,7 +648,7 @@ namespace Barotrauma }); if (canAccess) { - bool pressButton = buttonPressTimer <= 0 || lastDoor.door != door || lastDoor.state != shouldBeOpen; + bool pressButton = buttonPressTimer <= 0 || lastDoor.door != door || lastDoor.shouldBeOpen != shouldBeOpen; if (door.HasIntegratedButtons) { if (pressButton && character.CanSeeTarget(door.Item)) @@ -653,7 +656,7 @@ namespace Barotrauma if (door.Item.TryInteract(character, forceSelectKey: true)) { lastDoor = (door, shouldBeOpen); - buttonPressTimer = buttonPressCooldown; + buttonPressTimer = shouldBeOpen ? buttonPressCooldown : 0; } else { @@ -671,7 +674,7 @@ namespace Barotrauma if (closestButton.Item.TryInteract(character, forceSelectKey: true)) { lastDoor = (door, shouldBeOpen); - buttonPressTimer = buttonPressCooldown; + buttonPressTimer = shouldBeOpen ? buttonPressCooldown : 0; } else { @@ -697,7 +700,6 @@ namespace Barotrauma // The button is on the wrong side of the door or a wall currentPath.Unreachable = true; } - lastDoor = (null, false); return; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index 905cffa20..3d07e452f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -16,7 +16,6 @@ namespace Barotrauma public virtual bool IgnoreUnsafeHulls => false; public virtual bool AbandonWhenCannotCompleteSubjectives => true; public virtual bool AllowSubObjectiveSorting => false; - public virtual bool ForceOrderPriority => true; public virtual bool PrioritizeIfSubObjectivesActive => false; /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs index 42950891c..007aee4a5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItem.cs @@ -85,11 +85,12 @@ namespace Barotrauma bool equip = item.GetComponent() != null || item.AllowedSlots.Any(s => s != InvSlotType.Any) && item.AllowedSlots.None(s => - s == InvSlotType.Card || - s == InvSlotType.Head || - s == InvSlotType.Headset || - s == InvSlotType.InnerClothes || - s == InvSlotType.OuterClothes); + s == InvSlotType.Card || + s == InvSlotType.Head || + s == InvSlotType.Headset || + s == InvSlotType.InnerClothes || + s == InvSlotType.OuterClothes || + s == InvSlotType.HealthInterface); TryAddSubObjective(ref decontainObjective, () => new AIObjectiveDecontainItem(character, item, objectiveManager, targetContainer: suitableContainer.GetComponent()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs index f3197b975..c47cc92f8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCleanupItems.cs @@ -11,7 +11,7 @@ namespace Barotrauma public override string Identifier { get; set; } = "cleanup items"; public override bool KeepDivingGearOn => true; public override bool AllowAutomaticItemUnequipping => false; - public override bool ForceOrderPriority => false; + protected override bool ForceOrderPriority => false; public readonly List prioritizedItems = new List(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index de085d3c4..ac40cf6ad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -117,7 +117,7 @@ namespace Barotrauma private float AimSpeed => HumanAIController.AimSpeed; private float AimAccuracy => HumanAIController.AimAccuracy; - private bool EnemyIsClose() => Enemy != null && character.CurrentHull != null && character.CurrentHull == Enemy.CurrentHull || Vector2.DistanceSquared(character.Position, Enemy.Position) < 500; + private bool EnemyIsClose() => Enemy != null && Enemy.CurrentHull != null && HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull) && Math.Abs(character.WorldPosition.X - Enemy.WorldPosition.X) < 300; public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1, float coolDown = 10.0f) : base(character, objectiveManager, priorityModifier) @@ -366,7 +366,7 @@ namespace Barotrauma } } } - bool isAllowedToSeekWeapons = !EnemyIsClose() && character.TeamID != CharacterTeamType.FriendlyNPC && IsOffensiveOrArrest; + bool isAllowedToSeekWeapons = character.CurrentHull != null && !EnemyIsClose() && character.TeamID != CharacterTeamType.FriendlyNPC && IsOffensiveOrArrest; if (!isAllowedToSeekWeapons) { if (WeaponComponent == null) @@ -1190,19 +1190,5 @@ namespace Barotrauma } } } - - //private float CalculateEnemyStrength() - //{ - // float enemyStrength = 0; - // AttackContext currentContext = character.GetAttackContext(); - // foreach (Limb limb in Enemy.AnimController.Limbs) - // { - // if (limb.attack == null) continue; - // if (!limb.attack.IsValidContext(currentContext)) { continue; } - // if (!limb.attack.IsValidTarget(AttackTarget.Character)) { continue; } - // enemyStrength += limb.attack.GetTotalDamage(false); - // } - // return enemyStrength; - //} } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs index f39b105f7..e2ba2c782 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs @@ -159,7 +159,8 @@ namespace Barotrauma { TargetName = container.Item.Name, AbortCondition = obj => - container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI(character) || + container?.Item == null || container.Item.Removed || container.Item.IsThisOrAnyContainerIgnoredByAI(character) || + (container.Item.GetRootContainer()?.OwnInventory?.Locked ?? false) || ItemToContain == null || ItemToContain.Removed || !ItemToContain.IsOwnedBy(character) || container.Item.GetRootInventoryOwner() is Character c && c != character, SpeakIfFails = !objectiveManager.IsCurrentOrder() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs index 1c8b16955..dbcfad9d2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveDecontainItem.cs @@ -40,6 +40,7 @@ namespace Barotrauma public Func RemoveExistingPredicate { get; set; } public int? RemoveExistingMax { get; set; } public string AbandonGetItemDialogueIdentifier { get; set; } + public Func AbandonGetItemDialogueCondition { get; set; } public AIObjectiveDecontainItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, ItemContainer sourceContainer = null, ItemContainer targetContainer = null, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) @@ -106,6 +107,7 @@ namespace Barotrauma TryAddSubObjective(ref getItemObjective, constructor: () => new AIObjectiveGetItem(character, targetItem, objectiveManager, Equip) { + CannotFindDialogueCondition = AbandonGetItemDialogueCondition, CannotFindDialogueIdentifierOverride = AbandonGetItemDialogueIdentifier, SpeakIfFails = AbandonGetItemDialogueIdentifier != null, TakeWholeStack = this.TakeWholeStack diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs index 4de4c0a43..f4625b5bd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs @@ -26,6 +26,7 @@ namespace Barotrauma if (totalEnemies == 0) { return 0; } if (character.IsSecurity) { return 100; } if (objectiveManager.IsOrder(this)) { return 100; } + // If there's any security officers onboard, leave fighting for them. return HumanAIController.IsTrueForAnyCrewMember(c => c.Character.IsSecurity && !c.Character.IsIncapacitated && c.Character.Submarine == character.Submarine) ? 0 : 100; } @@ -64,6 +65,7 @@ namespace Barotrauma if (target.CurrentHull == null) { return false; } if (HumanAIController.IsFriendly(character, target)) { return false; } if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; } + if (character.Submarine.TeamID != target.Submarine.TeamID) { return false; } if (target.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { return false; } if (target.IsArrested) { return false; } return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index af2c65a9d..256913c4e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -162,7 +162,10 @@ namespace Barotrauma CloseEnough = reach, DialogueIdentifier = Leak.FlowTargetHull != null ? "dialogcannotreachleak" : null, TargetName = Leak.FlowTargetHull?.DisplayName, - CheckVisibility = false + CheckVisibility = false, + requiredCondition = () => Leak.Submarine == character.Submarine, + // The Go To objective can be abandoned if the leak is fixed (in which case we don't want to use the dialogue) + SpeakCannotReachCondition = () => !CheckObjectiveSpecific() }, onAbandon: () => { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index 6613d85c0..e6f4fb9b5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -59,6 +59,7 @@ namespace Barotrauma public bool CheckPathForEachItem { get; set; } public bool SpeakIfFails { get; set; } public string CannotFindDialogueIdentifierOverride { get; set; } + public Func CannotFindDialogueCondition { get; set; } private int _itemCount = 1; public int ItemCount @@ -560,22 +561,18 @@ namespace Barotrauma DebugConsole.NewMessage($"{character.Name}: Get item failed to reach {moveToTarget}", Color.Yellow); #endif } - if (SpeakIfFails) - { - SpeakCannotFind(); - } + SpeakCannotFind(); } private void SpeakCannotFind() { - if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective) - { - string msg = TextManager.Get(CannotFindDialogueIdentifierOverride, returnNull: true) ?? TextManager.Get("dialogcannotfinditem", returnNull: true); - if (msg != null) - { - character.Speak(msg, identifier: "dialogcannotfinditem", minDurationBetweenSimilar: 20.0f); - } - } + if (!SpeakIfFails) { return; } + if (!character.IsOnPlayerTeam) { return; } + if (objectiveManager.CurrentOrder != objectiveManager.CurrentObjective) { return; } + if (CannotFindDialogueCondition != null && !CannotFindDialogueCondition()) { return; } + string msg = TextManager.Get(CannotFindDialogueIdentifierOverride, returnNull: true) ?? TextManager.Get("dialogcannotfinditem", returnNull: true); + if (msg == null) { return; } + character.Speak(msg, identifier: "dialogcannotfinditem", minDurationBetweenSimilar: 20.0f); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index 72dcbaf98..6e1c554c4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -96,6 +96,8 @@ namespace Barotrauma public float? OverridePriority = null; + public Func SpeakCannotReachCondition { get; set; } + protected override float GetPriority() { bool isOrder = objectiveManager.IsOrder(this); @@ -166,14 +168,14 @@ namespace Barotrauma DebugConsole.NewMessage($"{character.Name}: Cannot reach the target: {Target}", Color.Yellow); } #endif - if (character.IsOnPlayerTeam && objectiveManager.CurrentOrder == objectiveManager.CurrentObjective && DialogueIdentifier != null && SpeakIfFails) - { - 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); - } - } + if (!character.IsOnPlayerTeam) { return; } + if (objectiveManager.CurrentOrder != objectiveManager.CurrentObjective) { return; } + if (DialogueIdentifier == null) { return; } + if (!SpeakIfFails) { return; } + if (SpeakCannotReachCondition != null && !SpeakCannotReachCondition()) { return; } + string msg = TargetName == null ? TextManager.Get(DialogueIdentifier, true) : TextManager.GetWithVariable(DialogueIdentifier, "[name]", TargetName, formatCapitals: !(Target is Character)); + if (msg == null) { return; } + character.Speak(msg, identifier: DialogueIdentifier, minDurationBetweenSimilar: 20.0f); } public void ForceAct(float deltaTime) => Act(deltaTime); @@ -635,21 +637,27 @@ namespace Barotrauma { get { - if (SteeringManager == PathSteering && PathSteering.CurrentPath?.CurrentNode?.Ladders != null) + if (SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.CurrentPath.Finished && PathSteering.IsCurrentNodeLadder) { - //don't consider the character to be close enough to the target while climbing ladders, - //UNLESS the last node in the path has been reached - //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; } + // Climbing a ladder + if (Target.WorldPosition.Y > character.WorldPosition.Y) + { + // The target is still above us + return false; + } + if (!character.AnimController.IsAboveFloor) + { + // Going through a hatch + return false; + } } if (!AlwaysUseEuclideanDistance && !character.AnimController.InWater) { - float yDiff = Math.Abs(Target.WorldPosition.Y - character.WorldPosition.Y); - if (yDiff > CloseEnough) { return false; } - float xDiff = Math.Abs(Target.WorldPosition.X - character.WorldPosition.X); - return xDiff <= CloseEnough; + float yDist = Math.Abs(Target.WorldPosition.Y - character.WorldPosition.Y); + if (yDist > CloseEnough) { return false; } + float xDist = Math.Abs(Target.WorldPosition.X - character.WorldPosition.X); + return xDist <= CloseEnough; } - Vector2 sourcePos = UseDistanceRelativeToAimSourcePos ? character.AnimController.AimSourceWorldPos : character.WorldPosition; return Vector2.DistanceSquared(Target.WorldPosition, sourcePos) < CloseEnough * CloseEnough; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs index 8a6b8cea7..64ea294d3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoadItem.cs @@ -4,6 +4,7 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; namespace Barotrauma { @@ -20,6 +21,8 @@ namespace Barotrauma private Item Container { get; } private ItemContainer ItemContainer { get; } private ImmutableArray TargetContainerTags { get; } + private ImmutableHashSet ValidContainableItemIdentifiers { get; } + private static Dictionary> AllValidContainableItemIdentifiers { get; } = new Dictionary>(); private int itemIndex = 0; private AIObjectiveDecontainItem decontainObjective; @@ -47,6 +50,109 @@ namespace Barotrauma abandonGetItemDialogueIdentifier = optionSpecificDialogueIdentifier; } } + ValidContainableItemIdentifiers = GetValidContainableItemIdentifiers(); + if (ValidContainableItemIdentifiers.None()) + { +#if DEBUG + DebugConsole.ShowError($"No valid containable item identifiers found for the Load Item objective targeting {Container}"); +#endif + Abandon = true; + return; + } + } + + private enum CheckStatus { Unfinished, Finished } + + private ImmutableHashSet GetValidContainableItemIdentifiers() + { + if (AllValidContainableItemIdentifiers.TryGetValue(Container.Prefab, out var existingIdentifiers)) + { + return existingIdentifiers; + } + // Status effects are often used to alter item condition so using the Containable Item Identifiers directly can lead to unwanted results + // For example, placing welding fuel tanks inside oxygen tank shelves + bool defaultContainableItemIdentifiers = true; + var potentialContainablePrefabs = MapEntityPrefab.List + .Where(mep => mep is ItemPrefab ip && ItemContainer.ContainableItemIdentifiers.Any(i => i == ip.Identifier || ip.Tags.Contains(i))) + .Cast(); + var validContainableItemIdentifiers = new HashSet(); + foreach (var component in Container.Components) + { + if (CheckComponent() == CheckStatus.Finished) + { + break; + } + CheckStatus CheckComponent() + { + if (component.statusEffectLists != null) + { + foreach (var (_, statusEffects) in component.statusEffectLists) + { + if (CheckStatusEffects(statusEffects) == CheckStatus.Finished) + { + return CheckStatus.Finished; + } + } + } + if (component is ItemContainer itemContainer && itemContainer.ContainableItems != null) + { + foreach (var item in itemContainer.ContainableItems) + { + if (CheckStatusEffects(item.statusEffects) == CheckStatus.Finished) + { + return CheckStatus.Finished; + } + } + } + return CheckStatus.Unfinished; + CheckStatus CheckStatusEffects(IEnumerable statusEffects) + { + if (statusEffects == null) { return CheckStatus.Unfinished; } + foreach (var statusEffect in statusEffects) + { + if ((statusEffect.TargetIdentifiers == null || statusEffect.TargetIdentifiers.None()) && !statusEffect.HasConditions) { continue; } + switch (TargetItemCondition) + { + case AIObjectiveLoadItems.ItemCondition.Empty: + if (!statusEffect.ReducesItemCondition()) { continue; } + break; + case AIObjectiveLoadItems.ItemCondition.Full: + if (!statusEffect.IncreasesItemCondition()) { continue; } + break; + default: + continue; + } + defaultContainableItemIdentifiers = false; + if (statusEffect.TargetIdentifiers != null) + { + foreach (string target in statusEffect.TargetIdentifiers) + { + foreach (var prefab in potentialContainablePrefabs) + { + if (CheckPrefab(prefab, () => prefab.Tags.Contains(target)) == CheckStatus.Finished) { return CheckStatus.Finished; } + } + } + } + foreach (var prefab in potentialContainablePrefabs) + { + if (CheckPrefab(prefab, () => statusEffect.MatchesTagConditionals(prefab)) == CheckStatus.Finished) { return CheckStatus.Finished; } + } + CheckStatus CheckPrefab(ItemPrefab prefab, Func isValid) + { + if (validContainableItemIdentifiers.Contains(prefab.Identifier)) { return CheckStatus.Unfinished; } + if (!isValid()) { return CheckStatus.Unfinished; } + validContainableItemIdentifiers.Add(prefab.Identifier); + if (potentialContainablePrefabs.Any(p => !validContainableItemIdentifiers.Contains(p.Identifier))) { return CheckStatus.Unfinished; } + return CheckStatus.Finished; + } + } + return CheckStatus.Unfinished; + } + } + } + var newIdentifiers = defaultContainableItemIdentifiers ? ItemContainer.ContainableItemIdentifiers.ToImmutableHashSet() : validContainableItemIdentifiers.ToImmutableHashSet(); + AllValidContainableItemIdentifiers.Add(Container.Prefab, newIdentifiers); + return newIdentifiers; } protected override float GetPriority() @@ -116,7 +222,7 @@ namespace Barotrauma base.Update(deltaTime); if (targetItem == null) { - if (character.FindItem(ref itemIndex, out Item item, identifiers: ItemContainer.ContainableItemIdentifiers, ignoreBroken: false, customPredicate: IsValidContainable, customPriorityFunction: GetConditionBasedPriority)) + if (character.FindItem(ref itemIndex, out Item item, identifiers: ValidContainableItemIdentifiers, ignoreBroken: false, customPredicate: IsValidContainable, customPriorityFunction: GetPriority)) { if (item == null) { @@ -125,17 +231,19 @@ namespace Barotrauma } targetItem = item; } - // Prefer items closer to full condition when target condition is Empty, and vice versa - float GetConditionBasedPriority(Item item) + float GetPriority(Item item) { try { - return TargetItemCondition switch + // Prefer items closer to full condition when target condition is Empty, and vice versa + float conditionBasedPriority = TargetItemCondition switch { AIObjectiveLoadItems.ItemCondition.Full => MathUtils.InverseLerp(100.0f, 0.0f, item.ConditionPercentage), AIObjectiveLoadItems.ItemCondition.Empty => MathUtils.InverseLerp(0.0f, 100.0f, item.ConditionPercentage), _ => throw new NotImplementedException() }; + // Prefer items that have the same identifier as one of the already contained items + return ItemContainer.ContainsItemsWithSameIdentifier(item) ? conditionBasedPriority : conditionBasedPriority / 2; } catch (NotImplementedException) { @@ -161,10 +269,11 @@ namespace Barotrauma TryAddSubObjective(ref decontainObjective, constructor: () => new AIObjectiveDecontainItem(character, targetItem, objectiveManager, targetContainer: ItemContainer, priorityModifier: PriorityModifier) { + AbandonGetItemDialogueCondition = () => IsValidContainable(targetItem), AbandonGetItemDialogueIdentifier = abandonGetItemDialogueIdentifier, Equip = true, RemoveExistingWhenNecessary = true, - RemoveExistingPredicate = (i) => AIObjectiveLoadItems.ItemMatchesTargetCondition(i, TargetItemCondition), + RemoveExistingPredicate = (i) => !ValidContainableItemIdentifiers.Contains(i.Prefab.Identifier) || AIObjectiveLoadItems.ItemMatchesTargetCondition(i, TargetItemCondition), RemoveExistingMax = 1 }, onCompleted: () => @@ -189,6 +298,7 @@ namespace Barotrauma { if (item == null) { return false; } if (item.Removed) { return false; } + if (!ValidContainableItemIdentifiers.Contains(item.Prefab.Identifier)) { return false; } if (ignoredItems.Contains(item)) { return false; } if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; } var rootInventoryOwner = item.GetRootInventoryOwner(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs index 21720599b..55b32ce12 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs @@ -46,6 +46,7 @@ namespace Barotrauma public override bool AllowSubObjectiveSorting => true; public virtual bool InverseTargetEvaluation => false; protected virtual bool ResetWhenClearingIgnoreList => true; + protected virtual bool ForceOrderPriority => true; public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace.CleanupStackTrace()); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index 1a24cc332..d521f19f7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -643,7 +643,12 @@ namespace Barotrauma public bool IsOrder(AIObjective objective) { - return objective == ForcedOrder || CurrentOrders.Any(o => o.Objective == objective); + if (objective == ForcedOrder) { return true; } + foreach (var order in CurrentOrders) + { + if (order.Objective == objective) { return true; } + } + return false; } public bool HasOrders() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs index 720cbd730..d0c4045e8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs @@ -33,6 +33,7 @@ namespace Barotrauma 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.IsAutoControlled) { return false; } if (pump.Item.ConditionPercentage <= 0) { return false; } if (pump.Item.CurrentHull.FireSources.Count > 0) { return false; } if (character.Submarine != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index 1c7786d6b..3b0b1499f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -489,7 +489,7 @@ namespace Barotrauma return Priority; } - public static IEnumerable GetSortedAfflictions(Character character) => CharacterHealth.SortAfflictionsBySeverity(character.CharacterHealth.GetAllAfflictions()); + public static IEnumerable GetSortedAfflictions(Character character, bool excludeBuffs = true) => CharacterHealth.SortAfflictionsBySeverity(character.CharacterHealth.GetAllAfflictions(), excludeBuffs); public static IEnumerable GetTreatableAfflictions(Character character) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index a460c39c0..ad361a9e1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -374,7 +374,7 @@ namespace Barotrauma } if (OptionNames.Count != Options.Length) { - DebugConsole.ThrowError("Error in Order " + Name + " - the number of option names doesn't match the number of options."); + DebugConsole.AddWarning("Error in Order " + Name + " - the number of option names doesn't match the number of options."); OptionNames.Clear(); Options.ForEach(o => OptionNames.Add(o, o)); } @@ -499,16 +499,14 @@ namespace Barotrauma return false; } - public string GetChatMessage(string targetCharacterName, string targetRoomName, bool givingOrderToSelf, string orderOption = "", int? priority = null) + public string GetChatMessage(string targetCharacterName, string targetRoomName, bool givingOrderToSelf, string orderOption = "", bool isNewOrder = true) { - priority ??= CharacterInfo.HighestManualOrderPriority; - // If the order has a lesser priority, it means we are rearranging character orders - if (!TargetAllCharacters && priority != CharacterInfo.HighestManualOrderPriority && Identifier != "dismissed") + if (!TargetAllCharacters && !isNewOrder && Identifier != "dismissed") { + // Use special dialogue when we're rearranging character orders return TextManager.GetWithVariable("rearrangedorders", "[name]", targetCharacterName ?? string.Empty, returnNull: true) ?? string.Empty; } - string messageTag = $"{(givingOrderToSelf && !TargetAllCharacters ? "OrderDialogSelf" : "OrderDialog")}"; - messageTag += $".{Identifier}"; + string messageTag = $"{(givingOrderToSelf && !TargetAllCharacters ? "OrderDialogSelf" : "OrderDialog")}.{Identifier}"; if (!string.IsNullOrEmpty(orderOption)) { if (Identifier != "dismissed") diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs index cb99e69b9..8aca8933a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs @@ -55,7 +55,7 @@ namespace Barotrauma CommandingCharacter.Speak(SuggestedOrderPrefab.GetChatMessage(OrderedCharacter.Name, "", false), minDurationBetweenSimilar: 5); } CurrentOrder = new Order(SuggestedOrderPrefab, TargetItem, TargetItemComponent, CommandingCharacter); - OrderedCharacter.SetOrder(CurrentOrder, Option, priority: 3, CommandingCharacter, CommandingCharacter != OrderedCharacter); + OrderedCharacter.SetOrder(CurrentOrder, Option, priority: CharacterInfo.HighestManualOrderPriority, CommandingCharacter, CommandingCharacter != OrderedCharacter); OrderedCharacter.Speak(TextManager.Get("DialogAffirmative"), delay: 1.0f, minDurationBetweenSimilar: 5); } TimeSinceLastAttempt = 0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs index 9ffb51694..83b02089e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs @@ -75,7 +75,7 @@ namespace Barotrauma public void Update(float deltaTime) { - if (!Active) { return; } + if (!Active || character.IsArrested) { return; } decisionTimer -= deltaTime; if (decisionTimer <= 0.0f) { @@ -344,7 +344,6 @@ namespace Barotrauma ShipIssueWorkers.Clear(); - // could have support for multiple reactors, todo m61 if (CommandedSubmarine.GetItems(false).Find(i => i.HasTag("reactor") && !i.NonInteractable)?.GetComponent() is Reactor reactor) { ShipIssueWorkers.Add(new ShipIssueWorkerPowerUpReactor(this, Order.GetPrefab("operatereactor"), reactor.Item, reactor, "powerup")); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index f9a0b6c2e..65597452e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -269,6 +269,11 @@ namespace Barotrauma } } + public float GetHeightFromFloor() => GetColliderBottom().Y - FloorY; + + // We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative. + public bool IsAboveFloor => GetHeightFromFloor() > -0.1f; + public void UpdateUseItem(bool allowMovement, Vector2 handWorldPos) { useItemTimer = 0.5f; @@ -332,7 +337,7 @@ namespace Barotrauma aimingMelee = aimMelee; if (character.Stun > 0.0f || character.IsIncapacitated) { - aim = false; + aim = false; } //calculate the handle positions diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs index e829475c3..26732196b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/FishAnimController.cs @@ -382,51 +382,55 @@ namespace Barotrauma mouthLimb.body.ApplyLinearImpulse(Vector2.UnitY * force * 2, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); mouthLimb.body.ApplyTorque(-force * 50); } - var jaw = GetLimb(LimbType.Jaw); - if (jaw != null) - { - jaw.body.ApplyTorque(-(float)Math.Sin(eatTimer * 150) * jaw.Mass * 25); - } - character.ApplyStatusEffects(ActionType.OnEating, deltaTime); - - float particleFrequency = MathHelper.Clamp(eatSpeed / 2, 0.02f, 0.5f); - if (Rand.Value() < particleFrequency / 6) + if (Character.CanEat) { - target.AnimController.MainLimb.AddDamage(target.SimPosition, dmg, 0, 0, false); - } - if (Rand.Value() < particleFrequency) - { - target.AnimController.MainLimb.AddDamage(target.SimPosition, 0, dmg, 0, false); - } - if (eatTimer % 1.0f < 0.5f && (eatTimer - deltaTime * eatSpeed) % 1.0f > 0.5f) - { - static bool CanBeSevered(LimbJoint j) => !j.IsSevered && j.CanBeSevered && j.LimbA != null && !j.LimbA.IsSevered && j.LimbB != null && !j.LimbB.IsSevered; - //keep severing joints until there is only one limb left - var nonSeveredJoints = target.AnimController.LimbJoints.Where(CanBeSevered); - if (nonSeveredJoints.None()) + var jaw = GetLimb(LimbType.Jaw); + if (jaw != null) { - //small monsters don't eat the contents of the character's inventory - if (Mass < target.AnimController.Mass) - { - target.Inventory?.AllItemsMod.ForEach(it => it?.Drop(dropper: null)); - } - - //only one limb left, the character is now full eaten - Entity.Spawner?.AddToRemoveQueue(target); - - if (Character.AIController is EnemyAIController enemyAi) - { - enemyAi.PetBehavior?.OnEat("dead", 1.0f); - } - - character.SelectedCharacter = null; + jaw.body.ApplyTorque(-(float)Math.Sin(eatTimer * 150) * jaw.Mass * 25); } - else //sever a random joint + + character.ApplyStatusEffects(ActionType.OnEating, deltaTime); + + float particleFrequency = MathHelper.Clamp(eatSpeed / 2, 0.02f, 0.5f); + if (Rand.Value() < particleFrequency / 6) { - target.AnimController.SeverLimbJoint(nonSeveredJoints.GetRandom()); + target.AnimController.MainLimb.AddDamage(target.SimPosition, dmg, 0, 0, false); } - } + if (Rand.Value() < particleFrequency) + { + target.AnimController.MainLimb.AddDamage(target.SimPosition, 0, dmg, 0, false); + } + if (eatTimer % 1.0f < 0.5f && (eatTimer - deltaTime * eatSpeed) % 1.0f > 0.5f) + { + static bool CanBeSevered(LimbJoint j) => !j.IsSevered && j.CanBeSevered && j.LimbA != null && !j.LimbA.IsSevered && j.LimbB != null && !j.LimbB.IsSevered; + //keep severing joints until there is only one limb left + var nonSeveredJoints = target.AnimController.LimbJoints.Where(CanBeSevered); + if (nonSeveredJoints.None()) + { + //small monsters don't eat the contents of the character's inventory + if (Mass < target.AnimController.Mass) + { + target.Inventory?.AllItemsMod.ForEach(it => it?.Drop(dropper: null)); + } + + //only one limb left, the character is now full eaten + Entity.Spawner?.AddToRemoveQueue(target); + + if (Character.AIController is EnemyAIController enemyAi) + { + enemyAi.PetBehavior?.OnEat("dead", 1.0f); + } + + character.SelectedCharacter = null; + } + else //sever a random joint + { + target.AnimController.SeverLimbJoint(nonSeveredJoints.GetRandom()); + } + } + } } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 109cec337..68a2054e4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -808,7 +808,8 @@ namespace Barotrauma if (head == null) { return; } if (torso == null) { return; } - if (currentHull != null) + //check both hulls: the hull whose coordinate space the ragdoll is in, and the hull whose bounds the character's origin actually is inside + if (currentHull != null && character.CurrentHull != null) { float surfacePos = currentHull.Surface; float surfaceThreshold = ConvertUnits.ToDisplayUnits(Collider.SimPosition.Y + 1.0f); @@ -816,7 +817,7 @@ namespace Barotrauma //and use its water surface instead of the current hull's if (currentHull.Rect.Y - currentHull.Surface < 5.0f) { - GetSurfacePos(CurrentHull, ref surfacePos); + GetSurfacePos(currentHull, ref surfacePos); void GetSurfacePos(Hull hull, ref float prevSurfacePos) { if (prevSurfacePos > surfaceThreshold) { return; } @@ -834,7 +835,7 @@ namespace Barotrauma foreach (var linkedTo in gap.linkedTo) { - if (linkedTo is Hull otherHull && otherHull != hull) + if (linkedTo is Hull otherHull && otherHull != hull && otherHull != currentHull) { prevSurfacePos = Math.Max(surfacePos, otherHull.Surface); GetSurfacePos(otherHull, ref prevSurfacePos); @@ -888,7 +889,6 @@ namespace Barotrauma { Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition); Vector2 diff = (mousePos - torso.SimPosition) * Dir; - TargetMovement = new Vector2(0.0f, -0.1f); float newRotation = MathUtils.VectorToAngle(diff); Collider.SmoothRotate(newRotation, CurrentSwimParams.SteerTorque * character.SpeedMultiplier); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index e07f73f0c..ced071a7b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -130,6 +130,9 @@ namespace Barotrauma set => _structureDamage = value; } + [Serialize(true, true), Editable] + public bool EmitStructureDamageParticles { get; private set; } + private float _itemDamage; [Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)] public float ItemDamage @@ -311,7 +314,7 @@ namespace Barotrauma return totalDamage * DamageMultiplier; } - public Attack(float damage, float bleedingDamage, float burnDamage, float structureDamage, float itemDamage, float range = 0.0f, float penetration = 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); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 1922a0453..ecccfd1db 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -682,8 +682,8 @@ namespace Barotrauma get { return CharacterHealth.BloodlossAmount; } set { - if (!MathUtils.IsValid(value)) return; - CharacterHealth.BloodlossAmount = MathHelper.Clamp(value, 0.0f, 100.0f); + if (!MathUtils.IsValid(value)) { return; } + CharacterHealth.BloodlossAmount = value; } } @@ -1830,7 +1830,7 @@ namespace Barotrauma 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(se))) { return false; } } } if (attack.Conditionals.Any(c => c.TargetSelf && !c.Matches(this))) { return false; } @@ -2270,17 +2270,18 @@ namespace Barotrauma } } - if (SelectedConstruction?.GetComponent()?.TargetItem == item || - HeldItems.Any(it => it.GetComponent()?.TargetItem == item)) - { - return true; - } - if (item.InteractDistance == 0.0f && !item.Prefab.Triggers.Any()) { return false; } Pickable pickableComponent = item.GetComponent(); if (pickableComponent != null && pickableComponent.Picker != this && pickableComponent.Picker != null && !pickableComponent.Picker.IsDead) { return false; } + if (SelectedConstruction?.GetComponent()?.TargetItem == item) { return true; } + //optimization: don't use HeldItems because it allocates memory and this method is executed very frequently + var heldItem1 = Inventory?.GetItemInLimbSlot(InvSlotType.RightHand); + if (heldItem1?.GetComponent()?.TargetItem == item) { return true; } + var heldItem2 = Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand); + if (heldItem2?.GetComponent()?.TargetItem == item) { return true; } + Vector2 characterDirection = Vector2.Transform(Vector2.UnitY, Matrix.CreateRotationZ(AnimController.Collider.Rotation)); Vector2 upperBodyPosition = Position + (characterDirection * 20.0f); @@ -3225,7 +3226,7 @@ namespace Barotrauma if (orderGiver != null) { - var abilityOrderedCharacter = new AbilityCharacter(this); + var abilityOrderedCharacter = new AbilityOrderedCharacter(this); orderGiver.CheckTalents(AbilityEffectType.OnGiveOrder, abilityOrderedCharacter); if (orderGiver.LastOrderedCharacter != this) @@ -3547,12 +3548,12 @@ namespace Barotrauma } #endif // Don't allow beheading for monster attacks, because it happens too frequently (crawlers/tigerthreshers etc attacking each other -> they will most often target to the head) - TrySeverLimbJoints(limbHit, attack.SeverLimbsProbability, attackResult.Damage, allowBeheading: attacker == null || attacker.IsHuman || attacker.IsPlayer); + TrySeverLimbJoints(limbHit, attack.SeverLimbsProbability, attackResult.Damage, allowBeheading: attacker == null || attacker.IsHuman || attacker.IsPlayer, attacker: attacker); return attackResult; } - public void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability, float damage, bool allowBeheading) + public void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability, float damage, bool allowBeheading, Character attacker = null) { if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } #if DEBUG @@ -3590,9 +3591,20 @@ namespace Barotrauma if (severed) { Limb otherLimb = joint.LimbA == targetLimb ? joint.LimbB : joint.LimbA; - otherLimb.body.ApplyLinearImpulse(targetLimb.LinearVelocity * targetLimb.Mass, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.5f); + otherLimb.body.ApplyLinearImpulse(targetLimb.LinearVelocity * targetLimb.Mass, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.5f); + if (attacker != null) + { + foreach (var statusEffect in statusEffects) + { + if (statusEffect.type == ActionType.OnSevered) { statusEffect.SetUser(attacker); } + } + foreach (var statusEffect in targetLimb.StatusEffects) + { + if (statusEffect.type == ActionType.OnSevered) { statusEffect.SetUser(attacker); } + } + } ApplyStatusEffects(ActionType.OnSevered, 1.0f); - targetLimb.ApplyStatusEffects(ActionType.OnSevered, 1.0f); + targetLimb.ApplyStatusEffects(ActionType.OnSevered, 1.0f); } } if (wasSevered && targetLimb.character.AIController is EnemyAIController enemyAI) @@ -3961,8 +3973,8 @@ namespace Barotrauma causeOfDeathAffliction?.Source ?? LastAttacker, LastDamageSource); OnDeath?.Invoke(this, CauseOfDeath); - var abilityKiller = new AbilityCharacter(CauseOfDeath.Killer); - CheckTalents(AbilityEffectType.OnDieToCharacter, abilityKiller); + var abilityCharacterKiller = new AbilityCharacterKiller(CauseOfDeath.Killer); + CheckTalents(AbilityEffectType.OnDieToCharacter, abilityCharacterKiller); if (GameMain.GameSession != null && Screen.Selected == GameMain.GameScreen) { @@ -4472,18 +4484,17 @@ namespace Barotrauma if (info == null) { return false; } info.UnlockedTalents.Add(talentPrefab.Identifier); if (characterTalents.Any(t => t.Prefab == talentPrefab)) { return false; } - #if SERVER GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.UpdateTalents }); #endif CharacterTalent characterTalent = new CharacterTalent(talentPrefab, this); - characterTalent.ActivateTalent(addingFirstTime); characterTalents.Add(characterTalent); + characterTalent.ActivateTalent(addingFirstTime); characterTalent.AddedThisRound = addingFirstTime; if (addingFirstTime) { - OnTalentGiven(talentPrefab.Identifier); + OnTalentGiven(talentPrefab); } return true; } @@ -4493,6 +4504,24 @@ namespace Barotrauma return info.UnlockedTalents.Contains(identifier); } + public bool HasUnlockedAllTalents() + { + if (TalentTree.JobTalentTrees.TryGetValue(Info.Job.Prefab.Identifier, out TalentTree talentTree)) + { + foreach (TalentSubTree talentSubTree in talentTree.TalentSubTrees) + { + foreach (TalentOption talentOption in talentSubTree.TalentOptionStages) + { + if (talentOption.Talents.None(t => HasTalent(t.Identifier))) + { + return false; + } + } + } + } + return true; + } + public static IEnumerable GetFriendlyCrew(Character character) { if (character is null) @@ -4552,7 +4581,7 @@ namespace Barotrauma } partial void OnMoneyChanged(int prevAmount, int newAmount); - partial void OnTalentGiven(string talentIdentifier); + partial void OnTalentGiven(TalentPrefab talentPrefab); /// /// This dictionary is used for stats that are required very frequently. Not very performant, but easier to develop with for now. @@ -4724,4 +4753,49 @@ namespace Barotrauma public Character Killer { get; set; } } + class AbilityAttackData : AbilityObject, IAbilityCharacter + { + public float DamageMultiplier { get; set; } = 1f; + public float AddedPenetration { get; set; } = 0f; + public List Afflictions { get; set; } + public bool ShouldImplode { get; set; } = false; + public Attack SourceAttack { get; } + public Character Character { get; set; } + public Character Attacker { get; set; } + + public AbilityAttackData(Attack sourceAttack, Character character) + { + SourceAttack = sourceAttack; + Character = character; + } + } + + class AbilityAttackResult : AbilityObject, IAbilityAttackResult + { + public AttackResult AttackResult { get; set; } + + public AbilityAttackResult(AttackResult attackResult) + { + AttackResult = attackResult; + } + } + + class AbilityCharacterKiller : AbilityObject, IAbilityCharacter + { + public AbilityCharacterKiller(Character character) + { + Character = character; + } + public Character Character { get; set; } + } + + class AbilityOrderedCharacter : AbilityObject, IAbilityCharacter + { + public AbilityOrderedCharacter(Character character) + { + Character = character; + } + public Character Character { get; set; } + } + } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index e2772ba1c..5edfd3d8d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -361,7 +361,7 @@ namespace Barotrauma public CharacterTeamType TeamID; - private readonly NPCPersonalityTrait personalityTrait; + private NPCPersonalityTrait personalityTrait; public const int MaxCurrentOrders = 3; public static int HighestManualOrderPriority => MaxCurrentOrders; @@ -568,7 +568,7 @@ namespace Barotrauma HasGenders = CharacterConfigElement.GetAttributeBool("genders", false); HasRaces = CharacterConfigElement.GetAttributeBool("races", false); SetGenderAndRace(randSync); - Job = (jobPrefab == null) ? Job.Random(Rand.RandSync.Unsynced) : new Job(jobPrefab, variant); + Job = (jobPrefab == null) ? Job.Random(Rand.RandSync.Unsynced) : new Job(jobPrefab, randSync, variant); HairColors = CharacterConfigElement.GetAttributeTupleArray("haircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray(); FacialHairColors = CharacterConfigElement.GetAttributeTupleArray("facialhaircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray(); SkinColors = CharacterConfigElement.GetAttributeTupleArray("skincolors", new (Color, float)[] { (new Color(255, 215, 200, 255), 100f) }).ToImmutableArray(); @@ -584,11 +584,10 @@ namespace Barotrauma } else { - name = ""; Name = GetRandomName(randSync); } OriginalName = !string.IsNullOrEmpty(originalName) ? originalName : Name; - personalityTrait = NPCPersonalityTrait.GetRandom(name + HeadSpriteId); + SetPersonalityTrait(); Salary = CalculateSalary(); if (ragdollFileName != null) { @@ -597,6 +596,11 @@ namespace Barotrauma LoadHeadAttachments(); } + private void SetPersonalityTrait() + { + personalityTrait = NPCPersonalityTrait.GetRandom(Name + HeadSpriteId); + } + public string GetRandomName(Rand.RandSync randSync) { string name = ""; @@ -1261,7 +1265,7 @@ namespace Barotrauma { int prevAmount = ExperiencePoints; - var experienceGainMultiplier = new AbilityValue(1f); + var experienceGainMultiplier = new AbilityExperienceGainMultiplier(1f); if (isMissionExperience) { Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplier); @@ -1858,18 +1862,27 @@ namespace Barotrauma } } - class AbilitySkillGain : AbilityObject, IAbilityValue, IAbilityString, IAbilityCharacter + class AbilitySkillGain : AbilityObject, IAbilityValue, IAbilitySkillIdentifier, IAbilityCharacter { - public AbilitySkillGain(float value, string abilityString, Character character, bool gainedFromAbility) + public AbilitySkillGain(float skillAmount, string skillIdentifier, Character character, bool gainedFromAbility) { - Value = value; - String = abilityString; + Value = skillAmount; + SkillIdentifier = skillIdentifier; Character = character; GainedFromAbility = gainedFromAbility; } public Character Character { get; set; } public float Value { get; set; } - public string String { get; set; } + public string SkillIdentifier { get; set; } public bool GainedFromAbility { get; } } + + class AbilityExperienceGainMultiplier : AbilityObject, IAbilityValue + { + public AbilityExperienceGainMultiplier(float experienceGainMultiplier) + { + Value = experienceGainMultiplier; + } + public float Value { get; set; } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index 3407a90ed..150116a25 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -67,6 +67,9 @@ namespace Barotrauma public Affliction(AfflictionPrefab prefab, float strength) { +#if CLIENT + prefab?.ReloadSoundsIfNeeded(); +#endif Prefab = prefab; PendingAdditionStrength = Prefab.GrainBurst; _strength = strength; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionBleeding.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionBleeding.cs index 2059661f5..f6da0bfd1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionBleeding.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionBleeding.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Barotrauma +namespace Barotrauma { class AfflictionBleeding : Affliction { @@ -15,6 +11,10 @@ namespace Barotrauma { base.Update(characterHealth, targetLimb, deltaTime); characterHealth.BloodlossAmount += Strength * (1.0f / 60.0f) * deltaTime; + if (Source != null) + { + characterHealth.BloodlossAffliction.Source = Source; + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index 3a7b01df7..a3c9eccc1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -52,10 +52,6 @@ namespace Barotrauma { if (state == value) { return; } state = value; - if (character != null && character == Character.Controlled) - { - UpdateMessages(); - } } } @@ -81,6 +77,9 @@ namespace Barotrauma base.Update(characterHealth, targetLimb, deltaTime); character = characterHealth.Character; if (character == null) { return; } + + UpdateMessages(); + if (!subscribedToDeathEvent) { character.OnDeath += CharacterDead; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 983c91471..fda2666a1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -363,6 +363,7 @@ namespace Barotrauma public readonly string Name, Description; public readonly string TranslationOverride; public readonly bool IsBuff; + public readonly float HealCostMultiplier; public readonly string CauseOfDeathDescription, SelfCauseOfDeathDescription; @@ -655,6 +656,7 @@ namespace Barotrauma Name = TextManager.Get("AfflictionName." + translationId, true) ?? element.GetAttributeString("name", ""); Description = TextManager.Get("AfflictionDescription." + translationId, true) ?? element.GetAttributeString("description", ""); IsBuff = element.GetAttributeBool("isbuff", false); + HealCostMultiplier = element.GetAttributeFloat(nameof(HealCostMultiplier).ToLowerInvariant(), 1f); if (element.Attribute("nameidentifier") != null) { @@ -677,7 +679,7 @@ namespace Barotrauma MaxStrength = element.GetAttributeFloat("maxstrength", 100.0f); GrainBurst = element.GetAttributeFloat(nameof(GrainBurst).ToLowerInvariant(), 0.0f); - ShowInHealthScannerThreshold = element.GetAttributeFloat("showinhealthscannerthreshold", Math.Max(ActivationThreshold, 0.05f)); + ShowInHealthScannerThreshold = element.GetAttributeFloat("showinhealthscannerthreshold", Math.Max(ActivationThreshold, AfflictionType == "talentbuff" ? float.MaxValue : 0.05f)); TreatmentThreshold = element.GetAttributeFloat("treatmentthreshold", Math.Max(ActivationThreshold, 5.0f)); DamageOverlayAlpha = element.GetAttributeFloat("damageoverlayalpha", 0.0f); @@ -751,6 +753,32 @@ namespace Barotrauma } } +#if CLIENT + public void ReloadSoundsIfNeeded() + { + foreach (var effect in effects) + { + foreach (var statusEffect in effect.StatusEffects) + { + foreach (var sound in statusEffect.Sounds) + { + if (sound.Sound == null) { Submarine.ReloadRoundSound(sound); } + } + } + } + foreach (var periodicEffect in periodicEffects) + { + foreach (var statusEffect in periodicEffect.StatusEffects) + { + foreach (var sound in statusEffect.Sounds) + { + if (sound.Sound == null) { Submarine.ReloadRoundSound(sound); } + } + } + } + } +#endif + public override string ToString() { return "AfflictionPrefab (" + Name + ")"; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index f99817499..b835eb5c2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -111,6 +111,7 @@ namespace Barotrauma private Affliction oxygenLowAffliction; private Affliction pressureAffliction; private Affliction stunAffliction; + public Affliction BloodlossAffliction { get => bloodlossAffliction; } public bool IsUnconscious { @@ -181,7 +182,7 @@ namespace Barotrauma public float BloodlossAmount { get { return bloodlossAffliction.Strength; } - set { bloodlossAffliction.Strength = MathHelper.Clamp(value, 0.0f, 100.0f); } + set { bloodlossAffliction.Strength = MathHelper.Clamp(value, 0, bloodlossAffliction.Prefab.MaxStrength); } } public float Stun @@ -324,7 +325,11 @@ namespace Barotrauma if (kvp.Key == affliction) { int limbHealthIndex = limbHealths.IndexOf(kvp.Value); - return Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == limbHealthIndex); + foreach (Limb limb in Character.AnimController.Limbs) + { + if (limb.HealthIndex == limbHealthIndex) { return limb; } + } + return null; } } return null; @@ -658,7 +663,7 @@ namespace Barotrauma newStrength = Math.Min(existingAffliction.Prefab.MaxStrength, newStrength); if (existingAffliction == stunAffliction) { Character.SetStun(newStrength, true, true); } existingAffliction.Strength = newStrength; - existingAffliction.Source = newAffliction.Source; + if (newAffliction.Source != null) { existingAffliction.Source = newAffliction.Source; } CalculateVitality(); if (Vitality <= MinVitality) { @@ -744,7 +749,6 @@ namespace Barotrauma Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.MovementSpeed)); - // maybe a bit of a hacky way to do this. should inquire if there is a better way. M61T if (Character.InWater) { Character.StackSpeedMultiplier(1f + Character.GetStatValue(StatTypes.SwimmingSpeed)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs index 7e11a575b..7fd0524ed 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs @@ -35,7 +35,7 @@ namespace Barotrauma public Skill PrimarySkill { get; } - public Job(JobPrefab jobPrefab, int variant = 0) + public Job(JobPrefab jobPrefab, Rand.RandSync randSync = Rand.RandSync.Unsynced, int variant = 0) { prefab = jobPrefab; Variant = variant; @@ -43,7 +43,7 @@ namespace Barotrauma skills = new Dictionary(); foreach (SkillPrefab skillPrefab in prefab.Skills) { - var skill = new Skill(skillPrefab); + var skill = new Skill(skillPrefab, randSync); skills.Add(skillPrefab.Identifier, skill); if (skillPrefab.IsPrimarySkill) { PrimarySkill = skill; } } @@ -79,7 +79,7 @@ namespace Barotrauma { var prefab = JobPrefab.Random(randSync); var variant = Rand.Range(0, prefab.Variants, randSync); - return new Job(prefab, variant); + return new Job(prefab, randSync, variant); } public float GetSkillLevel(string skillIdentifier) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs index b0dc3c7be..c82419886 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Skill.cs @@ -36,10 +36,10 @@ namespace Barotrauma public readonly float PriceMultiplier = 1.0f; - public Skill(SkillPrefab prefab) + public Skill(SkillPrefab prefab, Rand.RandSync randSync) { Identifier = prefab.Identifier; - level = Rand.Range(prefab.LevelRange.Start, prefab.LevelRange.End, Rand.RandSync.Server); + level = Rand.Range(prefab.LevelRange.Start, prefab.LevelRange.End, randSync); icon = GetIcon(); PriceMultiplier = prefab.PriceMultiplier; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index 6d7a8f929..d56202294 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -583,6 +583,8 @@ namespace Barotrauma private readonly List statusEffects = new List(); + public IEnumerable StatusEffects { get { return statusEffects; } } + public Limb(Ragdoll ragdoll, Character character, LimbParams limbParams) { this.ragdoll = ragdoll; @@ -756,8 +758,8 @@ namespace Barotrauma } if (attacker != null) { - var abilityAffliction = new AbilityAfflictionCharacter(newAffliction, character); - attacker.CheckTalents(AbilityEffectType.OnAddDamageAffliction, abilityAffliction); + var abilityAfflictionCharacter = new AbilityAfflictionCharacter(newAffliction, character); + attacker.CheckTalents(AbilityEffectType.OnAddDamageAffliction, abilityAfflictionCharacter); } if (applyAffliction) { @@ -1309,4 +1311,16 @@ namespace Barotrauma partial void LoadParamsProjSpecific(); } + + class AbilityAfflictionCharacter : AbilityObject, IAbilityAffliction, IAbilityCharacter + { + public AbilityAfflictionCharacter(Affliction affliction, Character character) + { + Affliction = affliction; + Character = character; + } + public Character Character { get; set; } + public Affliction Affliction { get; set; } + } + } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs index ff4f961f9..59481d908 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs @@ -612,7 +612,7 @@ namespace Barotrauma [Serialize(0f, true, description: "Width of the collider."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)] public float Width { get; set; } - [Serialize(10f, true, description: "The more the density the heavier the limb is."), Editable(MinValueFloat = 0, MaxValueFloat = 100, DecimalCount = 2)] + [Serialize(10f, true, description: "The more the density the heavier the limb is."), Editable(MinValueFloat = 0.01f, MaxValueFloat = 100, DecimalCount = 2)] public float Density { get; set; } [Serialize(false, true), Editable] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs index 93b267cd3..b45a81b25 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionAttackData.cs @@ -1,5 +1,6 @@ using Barotrauma.Items.Components; using System; +using System.Collections.Generic; using System.Linq; using System.Xml.Linq; @@ -7,15 +8,19 @@ namespace Barotrauma.Abilities { class AbilityConditionAttackData : AbilityConditionData { + [Flags] private enum WeaponType { Any = 0, Melee = 1, Ranged = 2, - HandheldRanged = 3, - Turret = 4 + HandheldRanged = 4, + Turret = 8, + NoWeapon = 16 }; + private static readonly List WeaponTypeValues = Enum.GetValues(typeof(WeaponType)).Cast().ToList(); + private readonly string itemIdentifier; private readonly string[] tags; private readonly WeaponType weapontype; @@ -65,27 +70,39 @@ namespace Barotrauma.Abilities if (weapontype != WeaponType.Any) { - switch (weapontype) + foreach (WeaponType wt in WeaponTypeValues) { - // it is possible that an item that has both a melee and a projectile component will return true - // even when not used as a melee/ranged weapon respectively - // attackdata should contain data regarding whether the attack is melee or not - case WeaponType.Melee: - return item?.GetComponent() != null; - case WeaponType.Ranged: - return item?.GetComponent() != null; - case WeaponType.HandheldRanged: - { - var projectile = item?.GetComponent(); - return projectile?.Launcher?.GetComponent() != null; - } - case WeaponType.Turret: - { - var projectile = item?.GetComponent(); - return projectile?.Launcher?.GetComponent() != null; - } + if (wt == WeaponType.Any || !weapontype.HasFlag(wt)) { continue; } + switch (wt) + { + // it is possible that an item that has both a melee and a projectile component will return true + // even when not used as a melee/ranged weapon respectively + // attackdata should contain data regarding whether the attack is melee or not + case WeaponType.Melee: + if (item?.GetComponent() != null) { return true; } + break; + case WeaponType.Ranged: + if (item?.GetComponent() != null) { return true; } + break; + case WeaponType.HandheldRanged: + { + var projectile = item?.GetComponent(); + if (projectile?.Launcher?.GetComponent() != null) { return true; } + } + break; + case WeaponType.Turret: + { + var projectile = item?.GetComponent(); + if (projectile?.Launcher?.GetComponent() != null) { return true; } + } + break; + case WeaponType.NoWeapon: + if (item == null) { return true; } + break; + } } - } + return false; + } return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemInSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemInSubmarine.cs new file mode 100644 index 000000000..330bcfcd2 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemInSubmarine.cs @@ -0,0 +1,38 @@ +using System.Xml.Linq; + +namespace Barotrauma.Abilities +{ + class AbilityConditionItemInSubmarine : AbilityConditionData + { + private readonly SubmarineType? submarineType; + + public AbilityConditionItemInSubmarine(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) + { + if (conditionElement.Attribute("submarinetype") != null) + { + submarineType = conditionElement.GetAttributeEnum("submarinetype", SubmarineType.Player); + } + } + + protected override bool MatchesConditionSpecific(AbilityObject abilityObject) + { + if ((abilityObject as IAbilityItem)?.Item is Item item) + { + if (item.Submarine == null) { return false; } + if (submarineType.HasValue) + { + return item.Submarine.Info?.Type == submarineType.Value; + } + else + { + return true; + } + } + else + { + LogAbilityConditionError(abilityObject, typeof(IAbilityItem)); + return false; + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemOutsideSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemOutsideSubmarine.cs deleted file mode 100644 index d23794f56..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemOutsideSubmarine.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Xml.Linq; - -namespace Barotrauma.Abilities -{ - class AbilityConditionItemOutsideSubmarine : AbilityConditionData - { - - public AbilityConditionItemOutsideSubmarine(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } - - protected override bool MatchesConditionSpecific(AbilityObject abilityObject) - { - if ((abilityObject as IAbilityItem)?.Item is Item item) - { - return item.Submarine == null || item.Submarine.TeamID != character.Info.TeamID; - } - else - { - LogAbilityConditionError(abilityObject, typeof(IAbilityItem)); - return false; - } - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemWreck.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemWreck.cs deleted file mode 100644 index 81d1b1d06..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItemWreck.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Xml.Linq; - -namespace Barotrauma.Abilities -{ - class AbilityConditionItemWreck : AbilityConditionData - { - - public AbilityConditionItemWreck(CharacterTalent characterTalent, XElement conditionElement) : base(characterTalent, conditionElement) { } - - protected override bool MatchesConditionSpecific(AbilityObject abilityObject) - { - if ((abilityObject as IAbilityItem)?.Item is Item item) - { - return item.Submarine?.Info?.IsWreck ?? false; - } - else - { - LogAbilityConditionError(abilityObject, typeof(IAbilityItem)); - return false; - } - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionSkill.cs index 5c368df8f..52d189213 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionSkill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionSkill.cs @@ -18,13 +18,13 @@ namespace Barotrauma.Abilities protected override bool MatchesConditionSpecific(AbilityObject abilityObject) { - if ((abilityObject as IAbilityString)?.String is string skillIdentifier) + if ((abilityObject as IAbilitySkillIdentifier)?.SkillIdentifier is string skillIdentifier) { return MatchesConditionSpecific(skillIdentifier); } else { - LogAbilityConditionError(abilityObject, typeof(IAbilityString)); + LogAbilityConditionError(abilityObject, typeof(IAbilitySkillIdentifier)); return false; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityInterfaces.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityInterfaces.cs index 8c552ad82..ef57527d6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityInterfaces.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityInterfaces.cs @@ -30,9 +30,9 @@ public Character Character { get; set; } } - interface IAbilityString + interface IAbilitySkillIdentifier { - public string String { get; set; } + public string SkillIdentifier { get; set; } } interface IAbilityAffliction diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs index db98b843d..6d7038f4c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs @@ -16,173 +16,4 @@ namespace Barotrauma.Abilities public Character Character { get; set; } } - class AbilityItem : AbilityObject, IAbilityItem - { - public AbilityItem(Item item) - { - Item = item; - } - public Item Item { get; set; } - } - - class AbilityValue : AbilityObject, IAbilityValue - { - public AbilityValue(float value) - { - Value = value; - } - public float Value { get; set; } - } - - class AbilityAffliction : AbilityObject, IAbilityAffliction - { - public AbilityAffliction(Affliction affliction) - { - Affliction = affliction; - } - public Affliction Affliction { get; set; } - } - - class AbilityAfflictionCharacter : AbilityObject, IAbilityAffliction, IAbilityCharacter - { - public AbilityAfflictionCharacter(Affliction affliction, Character character) - { - Affliction = affliction; - Character = character; - } - public Character Character { get; set; } - public Affliction Affliction { get; set; } - } - - class AbilityValueItem : AbilityObject, IAbilityValue, IAbilityItemPrefab - { - public AbilityValueItem(float value, ItemPrefab itemPrefab) - { - Value = value; - ItemPrefab = itemPrefab; - } - public float Value { get; set; } - public ItemPrefab ItemPrefab { get; set; } - } - - class AbilityItemPrefabItem : AbilityObject, IAbilityItem, IAbilityItemPrefab - { - public AbilityItemPrefabItem(Item item, ItemPrefab itemPrefab) - { - Item = item; - ItemPrefab = itemPrefab; - } - public Item Item { get; set; } - public ItemPrefab ItemPrefab { get; set; } - } - - class AbilityValueString : AbilityObject, IAbilityValue, IAbilityString - { - public AbilityValueString(float value, string abilityString) - { - Value = value; - String = abilityString; - } - public float Value { get; set; } - public string String { get; set; } - } - - class AbilityStringCharacter : AbilityObject, IAbilityCharacter, IAbilityString - { - public AbilityStringCharacter(string abilityString, Character character) - { - String = abilityString; - Character = character; - } - public Character Character { get; set; } - public string String { get; set; } - } - - class AbilityValueAffliction : AbilityObject, IAbilityValue, IAbilityAffliction - { - public AbilityValueAffliction(float value, Affliction affliction) - { - Value = value; - Affliction = affliction; - } - public float Value { get; set; } - public Affliction Affliction { get; set; } - } - - class AbilityValueMission : AbilityObject, IAbilityValue, IAbilityMission - { - public AbilityValueMission(float value, Mission mission) - { - Value = value; - Mission = mission; - } - public float Value { get; set; } - public Mission Mission { get; set; } - } - - class AbilityLocation : AbilityObject, IAbilityLocation - { - public AbilityLocation(Location location) - { - Location = location; - } - - public Location Location { get; set; } - } - - // this is an exception class that should only be passed in this form, so classes that use it should cast into it directly - class AbilityAttackData : AbilityObject, IAbilityCharacter - { - public float DamageMultiplier { get; set; } = 1f; - public float AddedPenetration { get; set; } = 0f; - public List Afflictions { get; set; } - public bool ShouldImplode { get; set; } = false; - public Attack SourceAttack { get; } - public Character Character { get; set; } - public Character Attacker { get; set; } - - public AbilityAttackData(Attack sourceAttack, Character character) - { - SourceAttack = sourceAttack; - Character = character; - } - } - - class AbilityApplyTreatment : AbilityObject, IAbilityCharacter, IAbilityItem - { - public Character Character { get; set; } - - public Character User { get; set; } - - public Item Item { get; set; } - - public AbilityApplyTreatment(Character user, Character target, Item item) - { - Character = target; - User = user; - Item = item; - } - } - - class AbilityAttackResult : AbilityObject, IAbilityAttackResult - { - public AttackResult AttackResult { get; set; } - - public AbilityAttackResult(AttackResult attackResult) - { - AttackResult = attackResult; - } - } - - class AbilityCharacterSubmarine : AbilityObject, IAbilityCharacter, IAbilitySubmarine - { - public AbilityCharacterSubmarine(Character character, Submarine submarine) - { - Character = character; - Submarine = submarine; - } - public Character Character { get; set; } - public Submarine Submarine { get; set; } - } - } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs index 0abc6dbc1..27d856553 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs @@ -87,7 +87,7 @@ namespace Barotrauma.Abilities DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not take a parameter for ApplyEffect in talent {CharacterTalent.DebugIdentifier}"); } - protected void LogabilityObjectMismatch() + protected void LogAbilityObjectMismatch() { DebugConsole.ThrowError($"Incompatible ability! Ability {this} is incompatitible with this type of ability effect type in talent {CharacterTalent.DebugIdentifier}"); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs index f2c500a54..9f37a0e03 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs @@ -23,7 +23,7 @@ namespace Barotrauma.Abilities } else { - LogabilityObjectMismatch(); + LogAbilityObjectMismatch(); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs index 5d073a068..e3896090c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs @@ -29,7 +29,7 @@ namespace Barotrauma.Abilities } else { - LogabilityObjectMismatch(); + LogAbilityObjectMismatch(); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs index 8b4245dc9..9b4700fe5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAttackData.cs @@ -41,7 +41,7 @@ namespace Barotrauma.Abilities } else { - LogabilityObjectMismatch(); + LogAbilityObjectMismatch(); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySpawnItemsToContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySpawnItemsToContainer.cs index b13e97638..919848c3a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySpawnItemsToContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySpawnItemsToContainer.cs @@ -37,7 +37,7 @@ namespace Barotrauma.Abilities } else { - LogabilityObjectMismatch(); + LogAbilityObjectMismatch(); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAlienHoarder.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAlienHoarder.cs index 7c7141a25..a6907ca41 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAlienHoarder.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityAlienHoarder.cs @@ -34,7 +34,7 @@ namespace Barotrauma.Abilities } else { - LogabilityObjectMismatch(); + LogAbilityObjectMismatch(); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs index 9bb79d132..2cc7a26f1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityApprenticeship.cs @@ -1,6 +1,4 @@ -using Microsoft.Xna.Framework; -using System; -using System.Xml.Linq; +using System.Xml.Linq; namespace Barotrauma.Abilities { @@ -18,7 +16,7 @@ namespace Barotrauma.Abilities if (abilityObject is AbilitySkillGain abilitySkillGain && abilitySkillGain.Character != Character) { if (ignoreAbilitySkillGain && abilitySkillGain.GainedFromAbility) { return; } - Character.Info?.IncreaseSkillLevel(abilitySkillGain.String, 1.0f, gainedFromAbility: true); + Character.Info?.IncreaseSkillLevel(abilitySkillGain.SkillIdentifier, 1.0f, gainedFromAbility: true); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityMultitasker.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityMultitasker.cs index 5cb3857fc..c66630989 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityMultitasker.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityMultitasker.cs @@ -13,7 +13,7 @@ namespace Barotrauma.Abilities protected override void ApplyEffect(AbilityObject abilityObject) { - if ((abilityObject as IAbilityString)?.String is string skillIdentifier) + if ((abilityObject as IAbilitySkillIdentifier)?.SkillIdentifier is string skillIdentifier) { if (skillIdentifier != lastSkillIdentifier) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs index 8d6c64ac5..2fbb95ec9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs @@ -9,7 +9,7 @@ namespace Barotrauma.Abilities class CharacterAbilityTandemFire : CharacterAbilityApplyStatusEffectsToNearestAlly { // this should just be its own class, misleading to inherit here - private string tag; + private readonly string tag; public CharacterAbilityTandemFire(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement) { tag = abilityElement.GetAttributeString("tag", ""); @@ -20,7 +20,7 @@ namespace Barotrauma.Abilities if (Character.SelectedConstruction == null || !Character.SelectedConstruction.HasTag(tag)) { return; } Character closestCharacter = null; - float closestDistance = float.MaxValue; + float closestDistance = squaredMaxDistance; foreach (Character crewCharacter in Character.GetFriendlyCrew(Character)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 927c9a533..90e0402f0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -984,8 +984,8 @@ namespace Barotrauma 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; - if (Level.Loaded.Type == LevelData.LevelType.Outpost) + if (Submarine.MainSub == null) { return; } + if (Level.Loaded?.Type == LevelData.LevelType.Outpost && GameMain.GameSession != null) { NewMessage("The teleportsub command is unavailable in outpost levels!", Color.Red); return; @@ -1001,6 +1001,11 @@ namespace Barotrauma } else if (args[0].Equals("start", StringComparison.OrdinalIgnoreCase)) { + if (Level.Loaded == null) + { + NewMessage("Can't teleport the sub to the start of the level (no level loaded).", Color.Red); + return; + } Vector2 pos = Level.Loaded.StartPosition; if (Level.Loaded.StartOutpost != null) { @@ -1010,6 +1015,11 @@ namespace Barotrauma } else { + if (Level.Loaded == null) + { + NewMessage("Can't teleport the sub to the end of the level (no level loaded).", Color.Red); + return; + } Vector2 pos = Level.Loaded.EndPosition; if (Level.Loaded.EndOutpost != null) { @@ -1189,6 +1199,7 @@ namespace Barotrauma { foreach (Item it in Item.ItemList) { + if (it.GetComponent() != null) { continue; } it.Condition = it.MaxCondition; } }, null, true)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs index 507d7b61f..718ad830a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs @@ -355,7 +355,7 @@ namespace Barotrauma IEnumerable crewCharacters = GameSession.GetSessionCrewCharacters(); // use multipliers here so that we can easily add them together without introducing multiplicative XP stacking - var experienceGainMultiplier = new AbilityValue(1f); + var experienceGainMultiplier = new AbilityExperienceGainMultiplier(1f); crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnAllyGainMissionExperience, experienceGainMultiplier)); crewCharacters.ForEach(c => experienceGainMultiplier.Value += c.GetStatValue(StatTypes.MissionExperienceGainMultiplier)); @@ -374,11 +374,11 @@ namespace Barotrauma #endif // apply money gains afterwards to prevent them from affecting XP gains - var moneyGainMission = new AbilityValueMission(1f, this); - crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnGainMissionMoney, moneyGainMission)); - crewCharacters.ForEach(c => moneyGainMission.Value += c.GetStatValue(StatTypes.MissionMoneyGainMultiplier)); + var missionMoneyGainMultiplier = new AbilityMissionMoneyGainMultiplier(this, 1f); + crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnGainMissionMoney, missionMoneyGainMultiplier)); + crewCharacters.ForEach(c => missionMoneyGainMultiplier.Value += c.GetStatValue(StatTypes.MissionMoneyGainMultiplier)); - campaign.Money += (int)(reward * moneyGainMission.Value); + campaign.Money += (int)(reward * missionMoneyGainMultiplier.Value); foreach (Character character in crewCharacters) { @@ -534,4 +534,16 @@ namespace Barotrauma cargoRoom.Rect.Y - cargoRoom.Rect.Height + itemPrefab.Size.Y / 2); } } + + class AbilityMissionMoneyGainMultiplier : AbilityObject, IAbilityValue, IAbilityMission + { + public AbilityMissionMoneyGainMultiplier(Mission mission, float moneyGainMultiplier) + { + Value = moneyGainMultiplier; + Mission = mission; + } + public float Value { get; set; } + public Mission Mission { get; set; } + } + } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index 21742b295..0735f65f6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -15,6 +15,7 @@ namespace Barotrauma private readonly float scatter; private readonly float offset; + private readonly float delayBetweenSpawns; private Vector2? spawnPos; @@ -92,6 +93,7 @@ namespace Barotrauma offset = prefab.ConfigElement.GetAttributeFloat("offset", 0); scatter = Math.Clamp(prefab.ConfigElement.GetAttributeFloat("scatter", 500), 0, 3000); + delayBetweenSpawns = prefab.ConfigElement.GetAttributeFloat("delaybetweenspawns", 0.1f); if (GameMain.NetworkMember != null) { @@ -538,7 +540,7 @@ namespace Barotrauma SwarmBehavior.CreateSwarm(monsters.Cast()); DebugConsole.NewMessage($"Spawned: {ToString()}. Strength: {StringFormatter.FormatZeroDecimal(monsters.Sum(m => m.Params.AI.CombatStrength))}.", Color.LightBlue, debugOnly: true); } - }, Rand.Range(0f, amount / 2f)); + }, delayBetweenSpawns * i); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs index 13c0fc231..5be24397f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs @@ -45,6 +45,8 @@ namespace Barotrauma requiredDestinationTypes = prefab.ConfigElement.GetAttributeStringArray("requireddestinationtypes", null); RequireBeaconStation = prefab.ConfigElement.GetAttributeBool("requirebeaconstation", false); + + GameAnalyticsManager.AddDesignEvent($"ScriptedEvent:{prefab.Identifier}:Start"); } public void AddTarget(string tag, Entity target) @@ -229,5 +231,11 @@ namespace Barotrauma } return false; } + + public override void Finished() + { + base.Finished(); + GameAnalyticsManager.AddDesignEvent($"ScriptedEvent:{prefab.Identifier}:Finished:{CurrentActionIndex}"); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs index bc72b77cf..89327c8f0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs @@ -133,7 +133,7 @@ namespace Barotrauma.Extensions return source.Count(predicate) > 1; } } - + public static IEnumerable ToEnumerable(this T item) { yield return item; @@ -196,5 +196,28 @@ namespace Barotrauma.Extensions } return -1; } + + /// + /// Same as FirstOrDefault but will always return null instead of default(T) when no element is found + /// + public static T? FirstOrNull(this IEnumerable source, Func predicate) where T : struct + { + if (source.FirstOrDefault(predicate) is var first && !first.Equals(default(T))) + { + return first; + } + + return null; + } + + public static T? FirstOrNull(this IEnumerable source) where T : struct + { + if (source.FirstOrDefault() is var first && !first.Equals(default(T))) + { + return first; + } + + return null; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs index 6d0e4d255..b4f8652c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsConsent.cs @@ -149,6 +149,13 @@ namespace Barotrauma SetConsent(Consent.Error); } + if (!SteamManager.IsInitialized) + { + DebugConsole.AddWarning("Error in GameAnalyticsManager.GetConsent: Could not get a Steam authentication ticket (not connected to Steam)."); + SetConsent(Consent.Error); + return; + } + string authTicketStr; try { @@ -183,7 +190,7 @@ namespace Barotrauma return; } - var response = ((Task)t).Result; + if (!t.TryGetResult(out IRestResponse response)) { return; } if (!CheckResponse(response)) { SetConsent(Consent.Error); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs index 71a7a7a68..7a0033bce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameAnalytics/GameAnalyticsManager.cs @@ -367,7 +367,6 @@ namespace Barotrauma + GameMain.Version.ToString() + exeName + ":" + ((exeHash?.ShortHash == null) ? "Unknown" : exeHash.ShortHash) + ":" - + AssemblyInfo.GitBranch + ":" + AssemblyInfo.GitRevision + ":" + buildConfiguration); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index 5aa20f202..aac23f1d6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -47,7 +47,9 @@ namespace Barotrauma { if (order.TargetEntity == null) { - DebugConsole.ThrowError("Attempted to add an order with no target entity to CrewManager!\n" + Environment.StackTrace.CleanupStackTrace()); + string message = $"Attempted to add a \"{order.Name}\" order with no target entity to CrewManager!\n{Environment.StackTrace.CleanupStackTrace()}"; + DebugConsole.AddWarning(message); + GameAnalyticsManager.AddErrorEventOnce("CrewManager.AddOrder:OrderTargetEntityNull", GameAnalyticsManager.ErrorSeverity.Error, message); return false; } @@ -185,6 +187,10 @@ namespace Barotrauma public void InitRound() { +#if CLIENT + GUIContextMenu.CurrentContextMenu = null; +#endif + characters.Clear(); List spawnWaypoints = null; @@ -437,17 +443,17 @@ namespace Barotrauma return filteredCharacters // 1. Prioritize those who are on the same submarine than the controlled character .OrderByDescending(c => Character.Controlled == null || c.Submarine == Character.Controlled.Submarine) - // 2. Prioritize those who have been given the same maintenance or operate order as now issued - .ThenByDescending(c => c.CurrentOrders.Any(o => - o.Order != null && o.Order.Identifier == order.Identifier && - (order.Category == OrderCategory.Maintenance || order.Category == OrderCategory.Operate))) + // 2. Prioritize those who are already ordered to operate the device + .ThenByDescending(c => order.Category == OrderCategory.Operate && c.CurrentOrders.Any(o => o.Order != null && o.Order.Identifier == order.Identifier && o.Order.TargetEntity == order.TargetEntity)) // 3. Prioritize those with the appropriate job for the order .ThenByDescending(c => order.HasAppropriateJob(c)) - // 4. Prioritize bots over player controlled characters + // 4. Prioritize those who don't yet have another Operate order of the same kind (which allows quick-assigning multiple Operate orders to different characters) + .ThenByDescending(c => order.Category == OrderCategory.Operate && c.CurrentOrders.None(o => o.Order != null && o.Order.Identifier == order.Identifier)) + // 5. Prioritize bots over player controlled characters .ThenByDescending(c => c.IsBot) - // 5. Use the priority value of the current objective + // 6. Use the priority value of the current objective .ThenBy(c => c.AIController is HumanAIController humanAI ? humanAI.ObjectiveManager.CurrentObjective?.Priority : 0) - // 6. Prioritize those with the best skill for the order + // 7. Prioritize those with the best skill for the order .ThenByDescending(c => c.GetSkillLevel(order.AppropriateSkill)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index ff3462958..5602ed210 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -78,10 +78,11 @@ namespace Barotrauma //there can be no events before this time has passed during the 1st campaign round const float FirstRoundEventDelay = 0.0f; - public enum InteractionType { None, Talk, Examine, Map, Crew, Store, Repair, Upgrade, PurchaseSub } + public enum InteractionType { None, Talk, Examine, Map, Crew, Store, Repair, Upgrade, PurchaseSub, MedicalClinic } public readonly CargoManager CargoManager; public UpgradeManager UpgradeManager; + public MedicalClinic MedicalClinic; public List Factions; @@ -176,6 +177,7 @@ namespace Barotrauma { Money = InitialMoney; CargoManager = new CargoManager(this); + MedicalClinic = new MedicalClinic(this); } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index 1191f30c4..785f21700 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -192,6 +192,37 @@ namespace Barotrauma } #endif } + + + public static List GetCampaignSubs() + { + bool isSubmarineVisible(SubmarineInfo s) + => !GameMain.NetworkMember.ServerSettings.HiddenSubs.Any(h + => s.Name.Equals(h, StringComparison.OrdinalIgnoreCase)); + + List availableSubs = + SubmarineInfo.SavedSubmarines + .Where(s => + s.IsCampaignCompatible + && isSubmarineVisible(s)) + .ToList(); + + if (!availableSubs.Any()) + { + //None of the available subs were marked as campaign-compatible, just include all visible subs + availableSubs.AddRange( + SubmarineInfo.SavedSubmarines + .Where(isSubmarineVisible)); + } + + if (!availableSubs.Any()) + { + //No subs are visible at all! Just make the selected one available + availableSubs.Add(GameMain.NetLobbyScreen.SelectedSub); + } + + return availableSubs; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index f1c9f4762..58a58c0a7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -411,9 +411,9 @@ namespace Barotrauma GameAnalyticsManager.ProgressionStatus.Start, GameMode?.Name ?? "none"); - string eventId = "StartRound:GameMode:" + (GameMode?.Name ?? "none") + ":"; + string eventId = "StartRound:" + (GameMode?.Preset?.Identifier ?? "none") + ":"; GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none")); - GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Name ?? "none")); + GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Preset?.Identifier ?? "none")); GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.CharacterInfos?.Count() ?? 0)); foreach (Mission mission in missions) { @@ -421,6 +421,17 @@ namespace Barotrauma } GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + (Level.Loaded?.Type.ToString() ?? "none")); GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier ?? "none")); + if (GameMode is CampaignMode campaignMode) + { + if (campaignMode.Map?.Radiation != null && campaignMode.Map.Radiation.Enabled) + { + GameAnalyticsManager.AddDesignEvent(eventId + "RadiationEnabled"); + } + else + { + GameAnalyticsManager.AddDesignEvent(eventId + "RadiationDisabled"); + } + } #if CLIENT if (GameMode is CampaignMode) { SteamAchievementManager.OnBiomeDiscovered(levelData.Biome); } @@ -457,6 +468,8 @@ namespace Barotrauma } } + ReadyCheck.ReadyCheckCooldown = DateTime.MinValue; + GUI.PreventPauseMenuToggle = false; HintManager.OnRoundStarted(); @@ -895,14 +908,7 @@ namespace Barotrauma ((CampaignMode)GameMode).Save(doc.Root); - try - { - doc.SaveSafe(filePath); - } - catch (Exception e) - { - DebugConsole.ThrowError("Saving gamesession to \"" + filePath + "\" failed!", e); - } + doc.SaveSafe(filePath, throwExceptions: true); } /*public void Load(XElement saveElement) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs new file mode 100644 index 000000000..1847ebf36 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/MedicalClinic.cs @@ -0,0 +1,348 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Barotrauma.Extensions; + +namespace Barotrauma +{ + internal partial class MedicalClinic + { + public enum NetworkHeader + { + REQUEST_AFFLICTIONS, + REQUEST_PENDING, + ADD_PENDING, + REMOVE_PENDING, + CLEAR_PENDING, + HEAL_PENDING + } + + public enum AfflictionSeverity + { + Low, + Medium, + High + } + + public enum MessageFlag + { + Response, // responding to your request + Announce // responding to someone else's request + } + + public enum HealRequestResult + { + Unknown, // everything is not ok + Success, // everything ok + InsufficientFunds, // not enough money + Refused // the outpost has refused to provide medical assistance + } + + [NetworkSerialize] + public struct NetHealRequest : INetSerializableStruct + { + public HealRequestResult Result; + } + + [NetworkSerialize] + public struct NetRemovedAffliction : INetSerializableStruct + { + public NetCrewMember CrewMember; + public NetAffliction Affliction; + } + + public struct NetPendingCrew : INetSerializableStruct + { + [NetworkSerialize(ArrayMaxSize = CrewManager.MaxCrewSize)] + public NetCrewMember[] CrewMembers; + } + + public struct NetAffliction : INetSerializableStruct + { + [NetworkSerialize] + public string Identifier; + + [NetworkSerialize] + public ushort Strength; + + [NetworkSerialize] + public ushort Price; + + public AfflictionSeverity AfflictionSeverity + { + get + { + if (Prefab is null) { return AfflictionSeverity.Low; } + + float normalizedStrength = Strength / Prefab.MaxStrength; + + // lesser than 0.1 + if (normalizedStrength <= 0.1) + { + return AfflictionSeverity.Low; + } + + // between 0.1 and 0.5 + if (normalizedStrength > 0.1f && normalizedStrength < 0.5f) + { + return AfflictionSeverity.Medium; + } + + // greater than 0.5 + return AfflictionSeverity.High; + } + } + + public Affliction Affliction + { + set + { + Identifier = value.Identifier; + Strength = (ushort)Math.Ceiling(value.Strength); + Price = (ushort)(Strength * value.Prefab.HealCostMultiplier); + } + } + + private AfflictionPrefab? cachedPrefab; + + public AfflictionPrefab? Prefab + { + get + { + if (cachedPrefab is { } cached) { return cached; } + + foreach (AfflictionPrefab prefab in AfflictionPrefab.List) + { + if (prefab.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase)) + { + cachedPrefab = prefab; + return prefab; + } + } + + return null; + } + set + { + cachedPrefab = value; + Identifier = value?.Identifier ?? string.Empty; + Strength = 0; + Price = 0; + } + } + + public readonly bool AfflictionEquals(AfflictionPrefab prefab) + { + return prefab.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase); + } + + public readonly bool AfflictionEquals(NetAffliction affliction) + { + return affliction.Identifier.Equals(Identifier, StringComparison.OrdinalIgnoreCase); + } + } + + public struct NetCrewMember : INetSerializableStruct + { + [NetworkSerialize] + public int CharacterInfoID; + + [NetworkSerialize] + public NetAffliction[] Afflictions; + + public CharacterInfo CharacterInfo + { + set => CharacterInfoID = value.GetIdentifierUsingOriginalName(); + } + + public readonly CharacterInfo? FindCharacterInfo(ImmutableArray crew) + { + foreach (CharacterInfo info in crew) + { + if (info.GetIdentifierUsingOriginalName() == CharacterInfoID) + { + return info; + } + } + + return null; + } + + public readonly bool CharacterEquals(NetCrewMember crewMember) + { + return crewMember.CharacterInfoID == CharacterInfoID; + } + } + + private readonly CampaignMode? campaign; + + public MedicalClinic(CampaignMode campaign) + { + this.campaign = campaign; + } + + public readonly List PendingHeals = new List(); + + public Action? OnUpdate; + + private static bool IsOutpostInCombat() + { + if (!(Level.Loaded is { Type: LevelData.LevelType.Outpost })) { return false; } + + IEnumerable crew = GetCrewCharacters().Where(c => c.Character != null).Select(c => c.Character).ToImmutableHashSet(); + + foreach (Character npc in Character.CharacterList.Where(c => c.TeamID == CharacterTeamType.FriendlyNPC)) + { + bool isInCombatWithCrew = !npc.IsInstigator && npc.AIController is HumanAIController { ObjectiveManager: { CurrentObjective: AIObjectiveCombat combatObjective } } && crew.Contains(combatObjective.Enemy); + if (isInCombatWithCrew) { return true; } + } + + return false; + } + + private HealRequestResult HealAllPending(bool force = false) + { + int totalCost = GetTotalCost(); + if (!force) + { + if (GetMoney() < totalCost) { return HealRequestResult.InsufficientFunds; } + + if (IsOutpostInCombat()) { return HealRequestResult.Refused; } + } + + ImmutableArray crew = GetCrewCharacters(); + foreach (NetCrewMember crewMember in PendingHeals) + { + CharacterInfo? targetCharacter = crewMember.FindCharacterInfo(crew); + if (!(targetCharacter?.Character is { CharacterHealth: { } health })) { continue; } + + foreach (NetAffliction affliction in crewMember.Afflictions) + { + health.ReduceAffliction(null, affliction.Identifier, affliction.Prefab?.MaxStrength ?? affliction.Strength); + } + } + + if (campaign != null) + { + campaign.Money -= totalCost; + } + + ClearPendingHeals(); + + return HealRequestResult.Success; + } + + private void ClearPendingHeals() + { + PendingHeals.Clear(); + } + + private void RemovePendingAffliction(NetCrewMember crewMember, NetAffliction affliction) + { + foreach (NetCrewMember listMember in PendingHeals.ToList()) + { + PendingHeals.Remove(listMember); + NetCrewMember pendingMember = listMember; + + if (pendingMember.CharacterEquals(crewMember)) + { + List newAfflictions = new List(); + foreach (NetAffliction pendingAffliction in pendingMember.Afflictions) + { + if (pendingAffliction.AfflictionEquals(affliction)) { continue; } + + newAfflictions.Add(pendingAffliction); + } + + pendingMember.Afflictions = newAfflictions.ToArray(); + } + + if (!pendingMember.Afflictions.Any()) { continue; } + + PendingHeals.Add(pendingMember); + } + } + + private void InsertPendingCrewMember(NetCrewMember crewMember) + { + if (PendingHeals.FirstOrNull(m => m.CharacterEquals(crewMember)) is { } foundHeal) + { + PendingHeals.Remove(foundHeal); + } + + PendingHeals.Add(crewMember); + } + + private NetAffliction[] GetAllAfflictions(CharacterHealth health) + { + IEnumerable rawAfflictions = health.GetAllAfflictions().Where(a => !a.Prefab.IsBuff && a.Strength > GetShowTreshold(a)); + + List afflictions = new List(); + + foreach (Affliction affliction in rawAfflictions) + { + NetAffliction newAffliction; + if (afflictions.FirstOrNull(netAffliction => netAffliction.AfflictionEquals(affliction.Prefab)) is { } foundAffliction) + { + afflictions.Remove(foundAffliction); + foundAffliction.Strength += (ushort)affliction.Strength; + foundAffliction.Price += (ushort)GetAdjustedPrice((int)(affliction.Prefab.HealCostMultiplier * affliction.Strength)); + newAffliction = foundAffliction; + } + else + { + newAffliction = new NetAffliction { Affliction = affliction }; + newAffliction.Price = (ushort)GetAdjustedPrice(newAffliction.Price); + } + + afflictions.Add(newAffliction); + } + + return afflictions.ToArray(); + + static float GetShowTreshold(Affliction affliction) => Math.Max(0, Math.Min(affliction.Prefab.ShowIconToOthersThreshold, affliction.Prefab.ShowInHealthScannerThreshold)); + } + + public int GetTotalCost() => PendingHeals.SelectMany(h => h.Afflictions).Aggregate(0, (current, affliction) => current + affliction.Price); + + private int GetAdjustedPrice(int price) => campaign?.Map?.CurrentLocation is { Type: { HasOutpost: true } } currentLocation ? currentLocation.GetAdjustedHealCost(price) : int.MaxValue; + + public int GetMoney() => campaign?.Money ?? 0; + + public static ImmutableArray GetCrewCharacters() + { +#if DEBUG && CLIENT + if (Screen.Selected is TestScreen) + { + return TestInfos.ToImmutableArray(); + } +#endif + + return Character.CharacterList.Where(c => c.Info != null && c.TeamID == CharacterTeamType.Team1).Select(c => c.Info).ToImmutableArray(); + } + +#if DEBUG && CLIENT + private static readonly CharacterInfo[] TestInfos = + { + new CharacterInfo("human"), + new CharacterInfo("human"), + new CharacterInfo("human"), + new CharacterInfo("human"), + new CharacterInfo("human"), + new CharacterInfo("human"), + new CharacterInfo("human") + }; + + private static readonly NetAffliction[] TestAfflictions = + { + new NetAffliction { Identifier = "internaldamage", Strength = 80, Price = 10 }, + new NetAffliction { Identifier = "blunttrauma", Strength = 50, Price = 10 }, + new NetAffliction { Identifier = "lacerations", Strength = 20, Price = 10 }, + new NetAffliction { Identifier = "burn", Strength = 10, Price = 10 } + }; +#endif + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index 2ff2e7d77..1689b9909 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -144,16 +144,6 @@ namespace Barotrauma.Items.Components if (linkedGap == null) { Rectangle rect = item.Rect; - if (IsHorizontal) - { - rect.Y += 5; - rect.Height += 10; - } - else - { - rect.X -= 5; - rect.Width += 10; - } linkedGap = new Gap(rect, !IsHorizontal, Item.Submarine) { Submarine = item.Submarine diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs index e73ea86f1..5ab363484 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/EntitySpawnerComponent.cs @@ -55,17 +55,22 @@ namespace Barotrauma.Items.Components [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 1f, ValueStep = 1f, DecimalCount = 0), Serialize("1,3", true, "Minumum and maximum amount of items or creatures to spawn in one attempt")] public Vector2 SpawnAmountRange { get; set; } - [Editable(MinValueInt = int.MinValue, MaxValueInt = int.MaxValue), Serialize(8, true, "Amount of items or creatures in the spawn area that will prevent further items or creatures from being spawned")] + [Editable(MinValueInt = 0, MaxValueInt = int.MaxValue), Serialize(8, true, "Total maximum amount of items or creatures that can be spawned. 0 = unrestricted.")] public int MaximumAmount { get; set; } - [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = int.MinValue, ValueStep = 10f), Serialize(500f, true, "Inflate the circle of rectangle by this value to extend the area that counts towards the maximum amount of items or enemies to be spawned")] + [Editable(MinValueInt = 0, MaxValueInt = int.MaxValue), Serialize(8, true, "Amount of items or creatures in the spawn area that will prevent further items or creatures from being spawned. 0 = unrestricted.")] + public int MaximumAmountInArea { get; set; } + + [Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize(500f, true, "Inflate the circle of rectangle by this value to extend the area that counts towards the maximum amount of items or enemies to be spawned")] public float MaximumAmountRangePadding { get; set; } [Serialize(true, true, "")] public bool CanSpawn { get; set; } = true; - private float SpawnTimer; - private float? SpawnTimerGoal; + private float spawnTimer; + private float? spawnTimerGoal; + + private int spawnedAmount = 0; public EntitySpawnerComponent(Item item, XElement element) : base(item, element) { @@ -115,15 +120,15 @@ namespace Barotrauma.Items.Components if (minTime < 0 && maxTime < 0) { return; } - SpawnTimerGoal ??= Rand.Range(minTime, maxTime, Rand.RandSync.Unsynced); + spawnTimerGoal ??= Rand.Range(minTime, maxTime, Rand.RandSync.Unsynced); - SpawnTimer += deltaTime; + spawnTimer += deltaTime; - if (SpawnTimer > SpawnTimerGoal) + if (spawnTimer > spawnTimerGoal) { Spawn(); - SpawnTimerGoal = null; - SpawnTimer = 0; + spawnTimerGoal = null; + spawnTimer = 0; } } @@ -149,12 +154,12 @@ namespace Barotrauma.Items.Components private RectangleF GetAreaRectangle(Vector2 size, Vector2 offset, bool draw) { Vector2 pos = item.WorldPosition; + pos += offset; if (draw) { pos.Y = -pos.Y; } - pos += offset; RectangleF rect = new RectangleF(pos.X - size.X / 2f, pos.Y - size.Y / 2f, size.X, size.Y); return rect; } @@ -162,6 +167,7 @@ namespace Barotrauma.Items.Components private bool CanSpawnMore() { if (!CanSpawn) { return false; } + if (MaximumAmount > 0 && spawnedAmount >= MaximumAmount) { return false; } if (OnlySpawnWhenCrewInRange) { @@ -171,10 +177,9 @@ namespace Barotrauma.Items.Components } } - if (MaximumAmount < 0) { return true; } + if (MaximumAmountInArea <= 0) { return true; } int amount; - if (!string.IsNullOrWhiteSpace(SpeciesName)) { amount = Character.CharacterList.Count(c => !c.IsDead && c.SpeciesName.Equals(SpeciesName, StringComparison.OrdinalIgnoreCase) && IsInRange(c.WorldPosition, crewArea: false, rangePad: true)); @@ -188,13 +193,12 @@ namespace Barotrauma.Items.Components return false; } - return amount < MaximumAmount; + return amount < MaximumAmountInArea; } private bool IsInRange(Vector2 worldPos, bool crewArea = false, bool rangePad = false) { Vector2 offset = crewArea ? CrewAreaOffset : SpawnAreaOffset; - offset.Y = -offset.Y; switch (crewArea ? CrewAreaShape : SpawnAreaShape) { case AreaShape.Circle: @@ -269,6 +273,7 @@ namespace Barotrauma.Items.Components string[] allSpecies = SpeciesName.Split(','); string species = allSpecies.GetRandom().Trim(); Entity.Spawner?.AddToSpawnQueue(species, pos); + spawnedAmount++; } else if (!string.IsNullOrWhiteSpace(ItemIdentifier)) { @@ -283,6 +288,7 @@ namespace Barotrauma.Items.Components } Entity.Spawner?.AddToSpawnQueue(prefab, pos, item.Submarine); + spawnedAmount++; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs index 636c96300..194f60d6d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/GeneticMaterial.cs @@ -74,6 +74,7 @@ namespace Barotrauma.Items.Components a.Identifier.Equals(Effect, StringComparison.OrdinalIgnoreCase) || a.AfflictionType.Equals(Effect, StringComparison.OrdinalIgnoreCase)).GetRandom(); } + Tainted = true; } [Serialize(3.0f, false)] @@ -94,37 +95,28 @@ namespace Barotrauma.Items.Components if (targetCharacter != null) { return; } - if (tainted) - { - if (selectedTaintedEffect != null) - { - float selectedTaintedEffectStrength = item.ConditionPercentage / 100.0f * selectedTaintedEffect.MaxStrength; - character.CharacterHealth.ApplyAffliction(null, selectedTaintedEffect.Instantiate(selectedTaintedEffectStrength)); - var existingAffliction = character.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedTaintedEffect); - if (existingAffliction != null) - { - existingAffliction.Strength = selectedTaintedEffectStrength; - } - targetCharacter = character; -#if SERVER - item.CreateServerEvent(this); -#endif - } - } if (selectedEffect != null) { - ApplyStatusEffects(ActionType.OnWearing, 1.0f); - float selectedEffectStrength = item.ConditionPercentage / 100.0f * selectedEffect.MaxStrength; - character.CharacterHealth.ApplyAffliction(null, selectedEffect.Instantiate(selectedEffectStrength)); - var existingAffliction = character.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedEffect); - if (existingAffliction != null) - { - existingAffliction.Strength = selectedEffectStrength; - } targetCharacter = character; + ApplyStatusEffects(ActionType.OnWearing, 1.0f); + float selectedEffectStrength = GetCombinedEffectStrength(); + character.CharacterHealth.ApplyAffliction(null, selectedEffect.Instantiate(selectedEffectStrength)); + var affliction = character.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedEffect); + if (affliction != null) { affliction.Strength = selectedEffectStrength; } #if SERVER item.CreateServerEvent(this); #endif + } + if (tainted && selectedTaintedEffect != null) + { + float selectedTaintedEffectStrength = GetCombinedTaintedEffectStrength(); + character.CharacterHealth.ApplyAffliction(null, selectedTaintedEffect.Instantiate(selectedTaintedEffectStrength)); + var affliction = character.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedTaintedEffect); + if (affliction != null) { affliction.Strength = selectedTaintedEffectStrength; } + targetCharacter = character; +#if SERVER + item.CreateServerEvent(this); +#endif } foreach (Item containedItem in item.ContainedItems) { @@ -142,13 +134,14 @@ namespace Barotrauma.Items.Components (rootContainer == null || !targetCharacter.HasEquippedItem(rootContainer) || !targetCharacter.Inventory.IsInLimbSlot(rootContainer, InvSlotType.HealthInterface))) { item.ApplyStatusEffects(ActionType.OnSevered, 1.0f, targetCharacter); - targetCharacter.CharacterHealth.ReduceAffliction(null, selectedEffect.Identifier, selectedEffect.MaxStrength); - if (tainted) - { - targetCharacter.CharacterHealth.ReduceAffliction(null, selectedTaintedEffect.Identifier, selectedTaintedEffect.MaxStrength); - } - targetCharacter = null; IsActive = false; + + var affliction = targetCharacter.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedEffect); + if (affliction != null) { affliction.Strength = GetCombinedEffectStrength(); } + var taintedAffliction = targetCharacter.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedTaintedEffect); + if (taintedAffliction != null) { taintedAffliction.Strength = GetCombinedTaintedEffectStrength(); } + + targetCharacter = null; } } } @@ -184,6 +177,36 @@ namespace Barotrauma.Items.Components } } + private float GetCombinedEffectStrength() + { + float effectStrength = 0.0f; + foreach (Item otherItem in targetCharacter.Inventory.FindAllItems(recursive: true)) + { + var geneticMaterial = otherItem.GetComponent(); + if (geneticMaterial == null || !geneticMaterial.IsActive) { continue; } + if (geneticMaterial.selectedEffect == selectedEffect) + { + effectStrength += otherItem.ConditionPercentage / 100.0f * selectedEffect.MaxStrength; + } + } + return effectStrength; + } + + private float GetCombinedTaintedEffectStrength() + { + float taintedEffectStrength = 0.0f; + foreach (Item otherItem in targetCharacter.Inventory.FindAllItems(recursive: true)) + { + var geneticMaterial = otherItem.GetComponent(); + if (geneticMaterial == null || !geneticMaterial.IsActive) { continue; } + if (selectedTaintedEffect != null && geneticMaterial.selectedTaintedEffect == selectedTaintedEffect) + { + taintedEffectStrength += otherItem.ConditionPercentage / 100.0f * selectedTaintedEffect.MaxStrength; + } + } + return taintedEffectStrength; + } + private float GetTaintedProbabilityOnRefine(Character user) { if (user == null) { return 1.0f; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs index bb67dea57..9e35e423b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Growable.cs @@ -278,7 +278,7 @@ namespace Barotrauma.Items.Components for (int i = 0, j = 0; i < maxSides; i++) { - if (!occupiedSides.IsBitSet((TileSide) (1 << i))) + if (!occupiedSides.HasFlag((TileSide) (1 << i))) { pool[j] = i; j++; @@ -303,7 +303,7 @@ namespace Barotrauma.Items.Components public bool CanGrowMore() => (Sides | BlockedSides).Count() < 4; - public bool IsSideBlocked(TileSide side) => BlockedSides.IsBitSet(side) || Sides.IsBitSet(side); + public bool IsSideBlocked(TileSide side) => BlockedSides.HasFlag(side) || Sides.HasFlag(side); public static Rectangle CreatePlantRect(Vector2 pos) => new Rectangle((int) pos.X - Size / 2, (int) pos.Y + Size / 2, Size, Size); } @@ -774,7 +774,7 @@ namespace Barotrauma.Items.Components TileSide oppositeSide = connectingSide.GetOppositeSide(); - if (otherVine.BlockedSides.IsBitSet(connectingSide)) + if (otherVine.BlockedSides.HasFlag(connectingSide)) { newVine.BlockedSides |= oppositeSide; continue; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index c03c26b72..99ef8136d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -166,7 +166,7 @@ namespace Barotrauma.Items.Components Pusher = new PhysicsBody(item.body.width, item.body.height, item.body.radius, item.body.Density) { BodyType = BodyType.Dynamic, - CollidesWith = Physics.CollisionCharacter, + CollidesWith = Physics.CollisionCharacter | Physics.CollisionProjectile, CollisionCategories = Physics.CollisionItemBlocking, Enabled = false, UserData = this @@ -604,7 +604,14 @@ namespace Barotrauma.Items.Components int maxAttachableCount = (int)character.Info.GetSavedStatValue(StatTypes.MaxAttachableCount, item.Prefab.Identifier); int currentlyAttachedCount = Item.ItemList.Count( i => i.Submarine == attachTarget?.Submarine && i.GetComponent() is Holdable holdable && holdable.Attached && i.Prefab.Identifier == item.prefab.Identifier); - if (currentlyAttachedCount >= maxAttachableCount) + if (maxAttachableCount == 0) + { +#if CLIENT + GUI.AddMessage(TextManager.Get("itemmsgrequiretraining"), Color.Red); +#endif + return false; + } + else if (currentlyAttachedCount >= maxAttachableCount) { #if CLIENT GUI.AddMessage($"{TextManager.Get("itemmsgtotalnumberlimited")} ({currentlyAttachedCount}/{maxAttachableCount})", Color.Red); @@ -801,7 +808,7 @@ namespace Barotrauma.Items.Components equipLimb = picker.AnimController.GetLimb(LimbType.Torso); } - if (equipLimb != null) + if (equipLimb != null && !equipLimb.Removed) { float itemAngle = (equipLimb.Rotation + holdAngle * picker.AnimController.Dir); @@ -814,6 +821,11 @@ namespace Barotrauma.Items.Components } } + public override void ReceiveSignal(Signal signal, Connection connection) + { + //do nothing + } + public override void FlipX(bool relativeToSub) { handlePos[0].X = -handlePos[0].X; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index 2233fd79a..ad88b23d0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -115,7 +115,7 @@ namespace Barotrauma.Items.Components reloadTimer /= (1f + item.GetQualityModifier(Quality.StatType.StrikingSpeedMultiplier)); item.body.FarseerBody.CollisionCategories = Physics.CollisionProjectile; - item.body.FarseerBody.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall; + item.body.FarseerBody.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionItemBlocking; item.body.FarseerBody.OnCollision += OnCollision; item.body.FarseerBody.IsBullet = true; item.body.PhysEnabled = true; @@ -361,6 +361,10 @@ namespace Barotrauma.Items.Components } hitTargets.Add(targetItem); } + else if (f2.Body.UserData is Holdable holdable && holdable.CanPush) + { + hitTargets.Add(holdable.Item); + } else { return false; @@ -411,6 +415,14 @@ namespace Barotrauma.Items.Components if (targetItem.Removed) { return; } Attack.DoDamage(User, targetItem, item.WorldPosition, 1.0f); } + else if (target.UserData is Holdable holdable && holdable.CanPush) + { + if (holdable.Item.Removed) { return; } + Attack.DoDamage(User, holdable.Item, item.WorldPosition, 1.0f); + RestoreCollision(); + hitting = false; + User = null; + } else { return; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs index f2561695e..93001af68 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Pickable.cs @@ -74,7 +74,7 @@ namespace Barotrauma.Items.Components if (PickingTime > 0.0f) { - var abilityPickingTime = new AbilityValueItem(PickingTime, item.Prefab); + var abilityPickingTime = new AbilityItemPickingTime(PickingTime, item.Prefab); picker.CheckTalents(AbilityEffectType.OnItemPicked, abilityPickingTime); if (requiredItems.ContainsKey(RelatedItem.RelationType.Equipped)) @@ -300,4 +300,15 @@ namespace Barotrauma.Items.Components } } } + + class AbilityItemPickingTime : AbilityObject, IAbilityValue, IAbilityItemPrefab + { + public AbilityItemPickingTime(float pickingTime, ItemPrefab itemPrefab) + { + Value = pickingTime; + ItemPrefab = itemPrefab; + } + public float Value { get; set; } + public ItemPrefab ItemPrefab { get; set; } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index bee423e3d..e3ff2e8be 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -158,7 +158,7 @@ namespace Barotrauma.Items.Components return MathHelper.ToRadians(spread); } - private readonly List limbBodies = new List(); + private readonly List ignoredBodies = new List(); public override bool Use(float deltaTime, Character character = null) { tryingToCharge = true; @@ -172,8 +172,8 @@ namespace Barotrauma.Items.Components if (character != null) { - var abilityItem = new AbilityItem(item); - character.CheckTalents(AbilityEffectType.OnUseRangedWeapon, abilityItem); + var abilityRangedWeapon = new AbilityRangedWeapon(item); + character.CheckTalents(AbilityEffectType.OnUseRangedWeapon, abilityRangedWeapon); } if (item.AiTarget != null) @@ -182,11 +182,20 @@ namespace Barotrauma.Items.Components item.AiTarget.SightRange = item.AiTarget.MaxSightRange; } - limbBodies.Clear(); + ignoredBodies.Clear(); foreach (Limb l in character.AnimController.Limbs) { if (l.IsSevered) { continue; } - limbBodies.Add(l.body.FarseerBody); + ignoredBodies.Add(l.body.FarseerBody); + } + + foreach (Item heldItem in character.HeldItems) + { + var holdable = heldItem.GetComponent(); + if (holdable?.Pusher != null) + { + ignoredBodies.Add(holdable.Pusher.FarseerBody); + } } float degreeOfFailure = 1.0f - DegreeOfSuccess(character); @@ -211,7 +220,7 @@ namespace Barotrauma.Items.Components } float damageMultiplier = 1f + item.GetQualityModifier(Quality.StatType.FirepowerMultiplier); projectile.Launcher = item; - projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: limbBodies.ToList(), createNetworkEvent: false, damageMultiplier); + projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: ignoredBodies.ToList(), createNetworkEvent: false, damageMultiplier); projectile.Item.GetComponent()?.Attach(Item, projectile.Item); if (i == 0) { @@ -270,4 +279,12 @@ namespace Barotrauma.Items.Components partial void LaunchProjSpecific(); } + class AbilityRangedWeapon : AbilityObject, IAbilityItem + { + public AbilityRangedWeapon(Item item) + { + Item = item; + } + public Item Item { get; set; } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index bc9c658ae..ef6174f36 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -521,7 +521,7 @@ namespace Barotrauma.Items.Components if (!fixableEntities.Contains("structure") && !fixableEntities.Contains(targetStructure.Prefab.Identifier)) { return true; } - ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, new ISerializableEntity[] { targetStructure }); + ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, structure: targetStructure); FixStructureProjSpecific(user, deltaTime, targetStructure, sectionIndex); float structureFixAmount = StructureFixAmount; @@ -589,8 +589,7 @@ namespace Barotrauma.Items.Components closestLimb.body.ApplyForce(dir * TargetForce, maxVelocity: 10.0f); } - ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, - closestLimb == null ? new ISerializableEntity[] { targetCharacter } : new ISerializableEntity[] { targetCharacter, closestLimb }); + ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, character: targetCharacter, limb: closestLimb); FixCharacterProjSpecific(user, deltaTime, targetCharacter); return true; } @@ -606,7 +605,7 @@ namespace Barotrauma.Items.Components } targetLimb.character.LastDamageSource = item; - ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, new ISerializableEntity[] { targetLimb.character, targetLimb }); + ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, character: targetLimb.character, limb: targetLimb); FixCharacterProjSpecific(user, deltaTime, targetLimb.character); return true; } @@ -645,7 +644,7 @@ namespace Barotrauma.Items.Components targetItem.IsHighlighted = true; - ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, targetItem.AllPropertyObjects); + ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, targetItem); if (targetItem.body != null && !MathUtils.NearlyEqual(TargetForce, 0.0f)) { @@ -682,7 +681,7 @@ namespace Barotrauma.Items.Components Reset(); return true; } - if (leak.Submarine == null) + if (leak.Submarine == null || leak.Submarine != character.Submarine) { Reset(); return true; @@ -836,32 +835,44 @@ namespace Barotrauma.Items.Components } } - private void ApplyStatusEffectsOnTarget(Character user, float deltaTime, ActionType actionType, IEnumerable targets) + private static List currentTargets = new List(); + private void ApplyStatusEffectsOnTarget(Character user, float deltaTime, ActionType actionType, Item targetItem = null, Character character = null, Limb limb = null, Structure structure = null) { if (statusEffectLists == null) { return; } if (!statusEffectLists.TryGetValue(actionType, out List statusEffects)) { return; } + currentTargets.Clear(); foreach (StatusEffect effect in statusEffects) { effect.SetUser(user); if (effect.HasTargetType(StatusEffect.TargetType.UseTarget)) { - effect.Apply(actionType, deltaTime, item, targets); + if (targetItem != null) + { + currentTargets.AddRange(targetItem.AllPropertyObjects); + } + if (structure != null) + { + currentTargets.Add(structure); + } + effect.Apply(actionType, deltaTime, item, currentTargets); } else if (effect.HasTargetType(StatusEffect.TargetType.Character)) { - effect.Apply(actionType, deltaTime, item, targets.Where(t => t is Character)); + currentTargets.Add(character); + effect.Apply(actionType, deltaTime, item, currentTargets); } else if (effect.HasTargetType(StatusEffect.TargetType.Limb)) { - effect.Apply(actionType, deltaTime, item, targets.Where(t => t is Limb)); + currentTargets.Add(limb); + effect.Apply(actionType, deltaTime, item, currentTargets); } #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) + foreach (ISerializableEntity target in currentTargets) { if (!(target is Door door)) { continue; } if (!door.CanBeWelded || !door.Item.IsInteractable(user)) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index 9f4fec7f2..45c52ad50 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -284,6 +284,43 @@ namespace Barotrauma.Items.Components SerializableProperties = SerializableProperty.DeserializeProperties(this, element); ParseMsg(); + string inheritRequiredSkillsFrom = element.GetAttributeString("inheritrequiredskillsfrom", ""); + if (!string.IsNullOrEmpty(inheritRequiredSkillsFrom)) + { + var component = item.Components.Find(ic => ic.Name.Equals(inheritRequiredSkillsFrom, StringComparison.OrdinalIgnoreCase)); + if (component == null) + { + DebugConsole.ThrowError($"Error in item \"{item.Name}\" - component \"{name}\" is set to inherit its required skills from \"{inheritRequiredSkillsFrom}\", but a component of that type couldn't be found."); + } + else + { + requiredSkills = component.requiredSkills; + } + } + + string inheritStatusEffectsFrom = element.GetAttributeString("inheritstatuseffectsfrom", ""); + if (!string.IsNullOrEmpty(inheritStatusEffectsFrom)) + { + var component = item.Components.Find(ic => ic.Name.Equals(inheritStatusEffectsFrom, StringComparison.OrdinalIgnoreCase)); + if (component == null) + { + DebugConsole.ThrowError($"Error in item \"{item.Name}\" - component \"{name}\" is set to inherit its StatusEffects from \"{inheritStatusEffectsFrom}\", but a component of that type couldn't be found."); + } + else if (component.statusEffectLists != null) + { + statusEffectLists ??= new Dictionary>(); + foreach (KeyValuePair> kvp in component.statusEffectLists) + { + if (!statusEffectLists.TryGetValue(kvp.Key, out List effectList)) + { + effectList = new List(); + statusEffectLists.Add(kvp.Key, effectList); + } + effectList.AddRange(kvp.Value); + } + } + } + foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -315,19 +352,8 @@ namespace Barotrauma.Items.Components requiredSkills.Add(new Skill(skillIdentifier, subElement.GetAttributeInt("level", 0))); break; case "statuseffect": - var statusEffect = StatusEffect.Load(subElement, item.Name); - - if (statusEffectLists == null) statusEffectLists = new Dictionary>(); - - List effectList; - if (!statusEffectLists.TryGetValue(statusEffect.type, out effectList)) - { - effectList = new List(); - statusEffectLists.Add(statusEffect.type, effectList); - } - - effectList.Add(statusEffect); - + statusEffectLists ??= new Dictionary>(); + LoadStatusEffect(subElement); break; default: if (LoadElemProjSpecific(subElement)) { break; } @@ -342,6 +368,17 @@ namespace Barotrauma.Items.Components break; } } + + void LoadStatusEffect(XElement subElement) + { + var statusEffect = StatusEffect.Load(subElement, item.Name); + if (!statusEffectLists.TryGetValue(statusEffect.type, out List effectList)) + { + effectList = new List(); + statusEffectLists.Add(statusEffect.type, effectList); + } + effectList.Add(statusEffect); + } } private void SetActiveState(bool isActive) @@ -399,6 +436,8 @@ namespace Barotrauma.Items.Components return false; } + public virtual bool UpdateWhenInactive => false; + //called when isActive is true and condition > 0.0f public virtual void Update(float deltaTime, Camera cam) { @@ -798,7 +837,10 @@ namespace Barotrauma.Items.Components foreach (ItemComponent ic in item.Components) { if (ic.statusEffectLists == null || !ic.statusEffectLists.TryGetValue(ActionType.OnBroken, out List brokenEffects)) { continue; } - brokenEffects.ForEach(e => e.SetUser(user)); + foreach (var brokenEffect in brokenEffects) + { + brokenEffect.SetUser(user); + } } } @@ -1007,7 +1049,8 @@ namespace Barotrauma.Items.Components return 0.0f; } } - return 1.0f; + // Prefer items with the same identifier as the contained items' + return container.ContainsItemsWithSameIdentifier(i) ? 1.0f : 0.5f; } }; containObjective.Abandoned += () => aiController.IgnoredItems.Add(container.Item); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index a7559b690..d172c7553 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -208,12 +208,13 @@ namespace Barotrauma.Items.Components public override bool RecreateGUIOnResolutionChange => true; + public List ContainableItems { get; } + public ItemContainer(Item item, XElement element) : base(item, element) { int totalCapacity = capacity; - List containableItems = null; foreach (XElement subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) @@ -225,8 +226,8 @@ namespace Barotrauma.Items.Components DebugConsole.ThrowError("Error in item config \"" + item.ConfigFile + "\" - containable with no identifiers."); continue; } - containableItems ??= new List(); - containableItems.Add(containable); + ContainableItems ??= new List(); + ContainableItems.Add(containable); break; case "subcontainer": totalCapacity += subElement.GetAttributeInt("capacity", 1); @@ -237,7 +238,7 @@ namespace Barotrauma.Items.Components slotRestrictions = new SlotRestrictions[totalCapacity]; for (int i = 0; i < capacity; i++) { - slotRestrictions[i] = new SlotRestrictions(maxStackSize, containableItems); + slotRestrictions[i] = new SlotRestrictions(maxStackSize, ContainableItems); } int subContainerIndex = capacity; @@ -344,6 +345,19 @@ namespace Barotrauma.Items.Components return slotRestrictions[index].MatchesItem(itemPrefab); } + public bool ContainsItemsWithSameIdentifier(Item item) + { + if (item == null) { return false; } + foreach (var containedItem in Inventory.AllItems) + { + if (containedItem.Prefab.Identifier == item.Prefab.Identifier) + { + return true; + } + } + return false; + } + readonly List targets = new List(); public override void Update(float deltaTime, Camera cam) @@ -432,7 +446,7 @@ namespace Barotrauma.Items.Components } } } - var abilityItem = new AbilityItem(item); + var abilityItem = new AbilityItemContainer(item); character.CheckTalents(AbilityEffectType.OnOpenItemContainer, abilityItem); return base.Select(character); @@ -494,6 +508,21 @@ namespace Barotrauma.Items.Components IsActive = true; } + public override void ReceiveSignal(Signal signal, Connection connection) + { + switch (connection.Name) + { + case "activate": + case "use": + case "trigger_in": + if (signal.value != "0") + { + item.Use(1.0f, signal.sender); + } + break; + } + } + public void SetContainedItemPositions() { Vector2 transformedItemPos = ItemPos * item.Scale; @@ -689,7 +718,6 @@ namespace Barotrauma.Items.Components } } - protected override void ShallowRemoveComponentSpecific() { } @@ -743,4 +771,13 @@ namespace Barotrauma.Items.Components return componentElement; } } + + class AbilityItemContainer : AbilityObject, IAbilityItem + { + public AbilityItemContainer(Item item) + { + Item = item; + } + public Item Item { get; set; } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index 77323447c..8e4a78fab 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -370,18 +370,29 @@ namespace Barotrauma.Items.Components public Item GetFocusTarget() { - item.SendSignal(new Signal(MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture), sender: user), "position_out"); - - for (int i = item.LastSentSignalRecipients.Count - 1; i >= 0; i--) + Item focusTarget = null; + for (int c = 0; c < 2; c++) { - if (item.LastSentSignalRecipients[i].Item.Condition <= 0.0f || item.LastSentSignalRecipients[i].IsPower) { continue; } - if (item.LastSentSignalRecipients[i].Item.Prefab.FocusOnSelected) + //try finding the item to focus on using trigger_out, and if that fails, using position_out + string connectionName = c == 0 ? "trigger_out" : "position_out"; + string signal = c == 0 ? "0" : MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture); + if (!item.SendSignal(new Signal(signal, sender: user), connectionName) || focusTarget != null) { - return item.LastSentSignalRecipients[i].Item; + continue; + } + + for (int i = item.LastSentSignalRecipients.Count - 1; i >= 0; i--) + { + if (item.LastSentSignalRecipients[i].Item.Condition <= 0.0f || item.LastSentSignalRecipients[i].IsPower) { continue; } + if (item.LastSentSignalRecipients[i].Item.Prefab.FocusOnSelected) + { + focusTarget = item.LastSentSignalRecipients[i].Item; + break; + } } } - - return null; + + return focusTarget; } public override bool Pick(Character picker) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index 38420fcd6..b887cd743 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -170,7 +170,7 @@ namespace Barotrauma.Items.Components character.CheckTalents(AbilityEffectType.OnItemDeconstructedByAlly, abilityTargetItem); } - var itemCreationMultiplier = new AbilityValueItem(amountMultiplier, targetItem.Prefab); + var itemCreationMultiplier = new AbilityItemCreationMultiplier(targetItem.Prefab, amountMultiplier); user.CheckTalents(AbilityEffectType.OnItemDeconstructedMaterial, itemCreationMultiplier); amountMultiplier = (int)itemCreationMultiplier.Value; } @@ -261,8 +261,8 @@ namespace Barotrauma.Items.Components if (user != null && !user.Removed) { // used to spawn items directly into the deconstructor - var itemContainer = new AbilityItemPrefabItem(item, targetItem.Prefab); - user.CheckTalents(AbilityEffectType.OnItemDeconstructedInventory, itemContainer); + var itemDeconstructedInventory = new AbilityItemDeconstructedInventory(targetItem.Prefab, item); + user.CheckTalents(AbilityEffectType.OnItemDeconstructedInventory, itemDeconstructedInventory); } int amount = (int)amountMultiplier; @@ -333,7 +333,7 @@ namespace Barotrauma.Items.Components for (int i = 0; i < outputContainer.Capacity; i++) { var containedItem = outputContainer.Inventory.GetItemAt(i); - if (containedItem?.OwnInventory != null && containedItem.OwnInventory.TryPutItem(item, user: null)) + if (containedItem?.OwnInventory != null && containedItem.GetComponent() == null && containedItem.OwnInventory.TryPutItem(item, user: null)) { return; } @@ -454,4 +454,26 @@ namespace Barotrauma.Items.Components public Character Character { get; set; } } + class AbilityItemCreationMultiplier : AbilityObject, IAbilityValue, IAbilityItemPrefab + { + public AbilityItemCreationMultiplier(ItemPrefab itemPrefab, float itemAmountMultiplier) + { + ItemPrefab = itemPrefab; + Value = itemAmountMultiplier; + } + public ItemPrefab ItemPrefab { get; set; } + public float Value { get; set; } + } + + class AbilityItemDeconstructedInventory : AbilityObject, IAbilityItem, IAbilityItemPrefab + { + public AbilityItemDeconstructedInventory(ItemPrefab itemPrefab, Item item) + { + ItemPrefab = itemPrefab; + Item = item; + } + public ItemPrefab ItemPrefab { get; set; } + public Item Item { get; set; } + } + } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index c29086343..6c3bf0ae7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -179,8 +179,6 @@ namespace Barotrauma.Items.Components if (selectedItem == null) { return; } if (!outputContainer.Inventory.CanBePut(selectedItem.TargetItem, selectedItem.OutCondition * selectedItem.TargetItem.Health)) { return; } - RefreshAvailableIngredients(); - #if CLIENT itemList.Enabled = false; activateButton.Text = TextManager.Get("FabricatorCancel"); @@ -189,7 +187,13 @@ namespace Barotrauma.Items.Components IsActive = true; this.user = user; fabricatedItem = selectedItem; - MoveIngredientsToInputContainer(selectedItem); + RefreshAvailableIngredients(); + + bool isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; + if (!isClient) + { + MoveIngredientsToInputContainer(selectedItem); + } requiredTime = GetRequiredTime(fabricatedItem, user); timeUntilReady = requiredTime; @@ -230,21 +234,19 @@ namespace Barotrauma.Items.Components } if (fabricatedItem == null) { return; } - fabricatedItem = null; - -#if CLIENT +#if SERVER + if (user != null) + { + GameServer.Log(GameServer.CharacterLogName(user) + " cancelled the fabrication of " + fabricatedItem.DisplayName + " in " + item.Name, ServerLog.MessageType.ItemInteraction); + } +#elif CLIENT itemList.Enabled = true; if (activateButton != null) { activateButton.Text = TextManager.Get("FabricatorCreate"); } #endif -#if SERVER - if (user != null) - { - GameServer.Log(GameServer.CharacterLogName(user) + " cancelled the fabrication of " + fabricatedItem.DisplayName + " in " + item.Name, ServerLog.MessageType.ItemInteraction); - } -#endif + fabricatedItem = null; } public override void Update(float deltaTime, Camera cam) @@ -256,15 +258,20 @@ namespace Barotrauma.Items.Components } refreshIngredientsTimer -= deltaTime; - if (fabricatedItem == null || !CanBeFabricated(fabricatedItem, availableIngredients, user)) + bool isClient = GameMain.NetworkMember?.IsClient ?? false; + + if (!isClient) { - CancelFabricating(); - return; + if (fabricatedItem == null || !CanBeFabricated(fabricatedItem, availableIngredients, user)) + { + CancelFabricating(); + return; + } } progressState = fabricatedItem == null ? 0.0f : (requiredTime - timeUntilReady) / requiredTime; - if (GameMain.NetworkMember?.IsClient ?? false) + if (isClient) { hasPower = State != FabricatorState.Paused; if (!hasPower) @@ -365,28 +372,29 @@ namespace Barotrauma.Items.Components availableItems.Remove(availableItem); Entity.Spawner.AddToRemoveQueue(availableItem); inputContainer.Inventory.RemoveItem(availableItem); + break; } } }); int amountFittingContainer = outputContainer.Inventory.HowManyCanBePut(fabricatedItem.TargetItem, fabricatedItem.OutCondition * fabricatedItem.TargetItem.Health); - var fabricationValueItem = new AbilityValueItem(fabricatedItem.Amount, fabricatedItem.TargetItem); + var fabricationitemAmount = new AbilityFabricationItemAmount(fabricatedItem.TargetItem, fabricatedItem.Amount); int quality = 0; if (user?.Info != null) { foreach (Character character in Character.GetFriendlyCrew(user)) { - character.CheckTalents(AbilityEffectType.OnAllyItemFabricatedAmount, fabricationValueItem); + character.CheckTalents(AbilityEffectType.OnAllyItemFabricatedAmount, fabricationitemAmount); } - user.CheckTalents(AbilityEffectType.OnItemFabricatedAmount, fabricationValueItem); + user.CheckTalents(AbilityEffectType.OnItemFabricatedAmount, fabricationitemAmount); quality = GetFabricatedItemQuality(fabricatedItem, user); } var tempUser = user; - for (int i = 0; i < (int)fabricationValueItem.Value; i++) + for (int i = 0; i < (int)fabricationitemAmount.Value; i++) { float outCondition = fabricatedItem.OutCondition; if (i < amountFittingContainer) @@ -433,7 +441,7 @@ namespace Barotrauma.Items.Components { float userSkill = user.GetSkillLevel(skill.Identifier); float addedSkill = skill.Level * SkillSettings.Current.SkillIncreasePerFabricatorRequiredSkill / Math.Max(userSkill, 1.0f); - var addedSkillValue = new AbilityValueString(addedSkill, skill.Identifier); + var addedSkillValue = new AbilityFabricatorSkillGain(skill.Identifier, addedSkill); user.CheckTalents(AbilityEffectType.OnItemFabricationSkillGain, addedSkillValue); user.Info.IncreaseSkillLevel( @@ -542,6 +550,11 @@ namespace Barotrauma.Items.Components private void RefreshAvailableIngredients() { + Character user = this.user; +#if CLIENT + user ??= Character.Controlled; +#endif + List itemList = new List(); itemList.AddRange(inputContainer.Inventory.AllItems); foreach (MapEntity linkedTo in item.linkedTo) @@ -550,6 +563,10 @@ namespace Barotrauma.Items.Components { var itemContainer = linkedItem.GetComponent(); if (itemContainer == null) { continue; } + if (user != null) + { + if (!itemContainer.HasRequiredItems(user, addMessage: false)) { continue; } + } var deconstructor = linkedItem.GetComponent(); if (deconstructor != null) @@ -568,17 +585,10 @@ namespace Barotrauma.Items.Components itemList.AddRange(container.Inventory.AllItems); } } -#if CLIENT - if (Character.Controlled?.Inventory != null) - { - itemList.AddRange(Character.Controlled.Inventory.AllItems); - } -#else if (user?.Inventory != null) { itemList.AddRange(user.Inventory.AllItems); } -#endif availableIngredients.Clear(); foreach (Item item in itemList) { @@ -600,8 +610,6 @@ namespace Barotrauma.Items.Components //required ingredients that are already present in the input container List usedItems = new List(); - bool isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; - targetItem.RequiredItems.ForEach(requiredItem => { for (int i = 0; i < requiredItem.Amount; i++) { @@ -630,10 +638,11 @@ namespace Barotrauma.Items.Components if (!inputContainer.Inventory.CanBePut(availablePrefab)) { var unneededItem = inputContainer.Inventory.AllItems.FirstOrDefault(it => !usedItems.Contains(it)); - unneededItem?.Drop(null, createNetworkEvent: !isClient); + unneededItem?.Drop(null); } - inputContainer.Inventory.TryPutItem(availablePrefab, user: null, createNetworkEvent: !isClient); + inputContainer.Inventory.TryPutItem(availablePrefab, user: null); } + break; } } }); @@ -684,5 +693,26 @@ namespace Barotrauma.Items.Components } savedFabricatedItem = null; } + class AbilityFabricatorSkillGain : AbilityObject, IAbilityValue, IAbilitySkillIdentifier + { + public AbilityFabricatorSkillGain(string skillIdentifier, float skillAmount) + { + SkillIdentifier = skillIdentifier; + Value = skillAmount; + } + public float Value { get; set; } + public string SkillIdentifier { get; set; } + } + + class AbilityFabricationItemAmount : AbilityObject, IAbilityValue, IAbilityItemPrefab + { + public AbilityFabricationItemAmount(ItemPrefab itemPrefab, float itemAmount) + { + ItemPrefab = itemPrefab; + Value = itemAmount; + } + public float Value { get; set; } + public ItemPrefab ItemPrefab { get; set; } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index 9317f68b9..e0a701b14 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -29,6 +29,15 @@ namespace Barotrauma.Items.Components } } + public float CurrentBrokenVolume + { + get + { + if (item.ConditionPercentage > 10.0f || !IsActive) { return 0.0f; } + return (1.0f - item.ConditionPercentage / 10.0f) * 100.0f; + } + } + private float pumpSpeedLockTimer, isActiveLockTimer; [Serialize(0.0f, true, description: "How fast the item is currently pumping water (-100 = full speed out, 100 = full speed in). Intended to be used by StatusEffect conditionals (setting this value in XML has no effect).")] @@ -72,6 +81,8 @@ namespace Barotrauma.Items.Components private const float TinkeringSpeedIncrease = 4.0f; + public override bool UpdateWhenInactive => true; + public Pump(Item item, XElement element) : base(item, element) { @@ -82,11 +93,15 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { + pumpSpeedLockTimer -= deltaTime; + isActiveLockTimer -= deltaTime; + + if (!IsActive) { return; } + currFlow = 0.0f; if (TargetLevel != null) { - pumpSpeedLockTimer -= deltaTime; float hullPercentage = 0.0f; if (item.CurrentHull != null) { hullPercentage = (item.CurrentHull.WaterVolume / item.CurrentHull.Volume) * 100.0f; } FlowPercentage = ((float)TargetLevel - hullPercentage) * 10.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs index 91d3d3b5f..02028708a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs @@ -76,7 +76,7 @@ namespace Barotrauma.Items.Components { if (lastUser == value) { return; } lastUser = value; - degreeOfSuccess = lastUser == null ? 0.0f : DegreeOfSuccess(lastUser); + degreeOfSuccess = lastUser == null ? 0.0f : Math.Min(DegreeOfSuccess(lastUser), 1.0f); LastUserWasPlayer = lastUser.IsPlayer; } } @@ -601,7 +601,7 @@ namespace Barotrauma.Items.Components if (!shutDown) { - float degreeOfSuccess = DegreeOfSuccess(character); + float degreeOfSuccess = Math.Min(DegreeOfSuccess(character), 1.0f); float refuelLimit = 0.3f; //characters with insufficient skill levels don't refuel the reactor if (degreeOfSuccess > refuelLimit) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs index c0a64ef76..cf63c3894 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs @@ -106,6 +106,13 @@ namespace Barotrauma.Items.Components set; } + [Editable, Serialize(false, false, description: "Should the sonar view be centered on the transducers or the submarine's center of mass. Only has an effect if UseTransducers is enabled.")] + public bool CenterOnTransducers + { + get; + set; + } + [Editable, Serialize(false, false, description: "Does the sonar have mineral scanning mode. " + "Only available in-game when the Item has no Steering component.")] public bool HasMineralScanner { get; set; } @@ -318,7 +325,7 @@ namespace Barotrauma.Items.Components Vector2 transducerPosSum = Vector2.Zero; foreach (ConnectedTransducer transducer in connectedTransducers) { - if (transducer.Transducer.Item.Submarine != null) + if (transducer.Transducer.Item.Submarine != null && CenterOnTransducers) { return transducer.Transducer.Item.Submarine.WorldPosition; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index 44e6461b3..0707d3e1f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -311,7 +311,7 @@ namespace Barotrauma.Items.Components } // override autopilot pathing while the AI rams, and go full speed ahead - if (AIRamTimer > 0f) + if (AIRamTimer > 0f && controlledSub != null) { AIRamTimer -= deltaTime; TargetVelocity = GetSteeringVelocity(AITacticalTarget, 0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs index 3c999c05c..9d7002ac2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs @@ -1,7 +1,6 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; -using System.Collections.Generic; using System.Globalization; using System.Xml.Linq; @@ -111,6 +110,17 @@ namespace Barotrauma.Items.Components } } + [Serialize(false, true, description: "If true, the recharge speed (and power consumption) of the device goes up exponentially as the recharge rate is increased.")] + public bool ExponentialRechargeSpeed { get; set; } + + private float efficiency; + [Editable(minValue: 0.0f, maxValue: 1.0f, decimals: 2), Serialize(0.95f, true, description: "The amount of power you can get out of a item relative to the amount of power that's put into it.")] + public float Efficiency + { + get { return efficiency; } + set { efficiency = MathHelper.Clamp(value, 0.0f, 1.0f); } + } + public float RechargeRatio => RechargeSpeed / MaxRechargeSpeed; public const float aiRechargeTargetRatio = 0.5f; @@ -170,7 +180,7 @@ namespace Barotrauma.Items.Components { ApplyStatusEffects(ActionType.OnActive, deltaTime, null); } - + if (charge >= capacity) { //rechargeVoltage = 0.0f; @@ -181,13 +191,17 @@ namespace Barotrauma.Items.Components { float missingCharge = capacity - charge; float targetRechargeSpeed = rechargeSpeed; + if (ExponentialRechargeSpeed) + { + targetRechargeSpeed = MathF.Pow(rechargeSpeed / maxRechargeSpeed, 2) * maxRechargeSpeed; + } if (missingCharge < 1.0f) { targetRechargeSpeed *= missingCharge; } currPowerConsumption = MathHelper.Lerp(currPowerConsumption, targetRechargeSpeed, 0.05f); - Charge += currPowerConsumption * Math.Min(Voltage, 1.0f) / 3600.0f; - } + Charge += currPowerConsumption * Math.Min(Voltage, 1.0f) / 3600.0f * efficiency; + } if (charge <= 0.0f) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs index 52c065e75..b69425e0e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerTransfer.cs @@ -10,6 +10,8 @@ namespace Barotrauma.Items.Components { public List PowerConnections { get; private set; } + private readonly HashSet signalConnections = new HashSet(); + private readonly Dictionary connectionDirty = new Dictionary(); //a list of connections a given connection is connected to, either directly or via other power transfer components @@ -121,6 +123,7 @@ namespace Barotrauma.Items.Components partial void InitProjectSpecific(XElement element); + private static readonly HashSet recipientsToRefresh = new HashSet(); public override void UpdateBroken(float deltaTime, Camera cam) { base.UpdateBroken(deltaTime, cam); @@ -132,7 +135,8 @@ namespace Barotrauma.Items.Components powerLoad = 0.0f; currPowerConsumption = 0.0f; SetAllConnectionsDirty(); - foreach (HashSet recipientList in connectedRecipients.Values.ToList()) + recipientsToRefresh.Clear(); + foreach (HashSet recipientList in connectedRecipients.Values) { foreach (Connection c in recipientList) { @@ -140,16 +144,26 @@ namespace Barotrauma.Items.Components var recipientPowerTransfer = c.Item.GetComponent(); if (recipientPowerTransfer != null) { - recipientPowerTransfer.SetAllConnectionsDirty(); - recipientPowerTransfer.RefreshConnections(); + recipientsToRefresh.Add(recipientPowerTransfer); } } } + foreach (PowerTransfer recipientPowerTransfer in recipientsToRefresh) + { + recipientPowerTransfer.SetAllConnectionsDirty(); + recipientPowerTransfer.RefreshConnections(); + } RefreshConnections(); isBroken = true; } } + + private int prevSentPowerValue; + private string powerSignal; + private int prevSentLoadValue; + private string loadSignal; + public override void Update(float deltaTime, Camera cam) { RefreshConnections(); @@ -172,6 +186,19 @@ namespace Barotrauma.Items.Components //if the item can't be fixed, don't allow it to break if (!item.Repairables.Any() || !CanBeOverloaded) { return; } + if (prevSentPowerValue != (int)-CurrPowerConsumption || powerSignal == null) + { + prevSentPowerValue = (int)Math.Round(-CurrPowerConsumption); + powerSignal = prevSentPowerValue.ToString(); + } + if (prevSentLoadValue != (int)powerLoad || loadSignal == null) + { + prevSentLoadValue = (int)Math.Round(powerLoad); + loadSignal = prevSentLoadValue.ToString(); + } + item.SendSignal(powerSignal, "power_value_out"); + item.SendSignal(loadSignal, "load_value_out"); + float maxOverVoltage = Math.Max(OverloadVoltage, 1.0f); Overload = -currPowerConsumption > Math.Max(powerLoad, 200.0f) * maxOverVoltage; if (Overload && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)) @@ -217,6 +244,7 @@ namespace Barotrauma.Items.Components return picker != null; } + private static readonly HashSet tempConnected = new HashSet(); protected void RefreshConnections() { var connections = item.Connections; @@ -229,15 +257,15 @@ namespace Barotrauma.Items.Components else if (!connectionDirty[c]) { continue; - } + } //find all connections that are connected to this one (directly or via another PowerTransfer) - HashSet connected = new HashSet(); + tempConnected.Clear(); if (item.Condition > 0.0f) { if (!connectedRecipients.ContainsKey(c)) { - connectedRecipients.Add(c, connected); + connectedRecipients.Add(c, tempConnected); } else { @@ -249,24 +277,22 @@ namespace Barotrauma.Items.Components } } - connected.Add(c); - GetConnected(c, connected); + tempConnected.Add(c); + GetConnected(c, tempConnected); } - connectedRecipients[c] = connected; + connectedRecipients[c] = tempConnected; //go through all the PowerTransfers that we're connected to and set their connections to match the ones we just calculated //(no need to go through the recursive GetConnected method again) - foreach (Connection recipient in connected) + foreach (Connection recipient in tempConnected) { + if (recipient == c) { continue; } var recipientPowerTransfer = recipient.Item.GetComponent(); - if (recipientPowerTransfer == null) continue; - + if (recipientPowerTransfer == null) { continue; } if (!connectedRecipients.ContainsKey(recipient)) { - connectedRecipients.Add(recipient, connected); + connectedRecipients.Add(recipient, tempConnected); } - - recipientPowerTransfer.connectedRecipients[recipient] = connected; recipientPowerTransfer.connectionDirty[recipient] = false; } } @@ -296,7 +322,7 @@ namespace Barotrauma.Items.Components public void SetAllConnectionsDirty() { - if (item.Connections == null) return; + if (item.Connections == null) { return; } foreach (Connection c in item.Connections) { connectionDirty[c] = true; @@ -321,6 +347,14 @@ namespace Barotrauma.Items.Components return; } + foreach (Connection c in connections) + { + if (c.Name.Length > 5 && c.Name.Substring(0, 6) == "signal") + { + signalConnections.Add(c); + } + } + if (!(this is RelayComponent)) { if (PowerConnections.Any(p => !p.IsOutput) && PowerConnections.Any(p => p.IsOutput)) @@ -356,29 +390,30 @@ namespace Barotrauma.Items.Components { if (item.Condition <= 0.0f || connection.IsPower) { return; } if (!connectedRecipients.ContainsKey(connection)) { return; } + if (!signalConnections.Contains(connection)) { return; } - if (connection.Name.Length > 5 && connection.Name.Substring(0, 6) == "signal") + foreach (Connection recipient in connectedRecipients[connection]) { - foreach (Connection recipient in connectedRecipients[connection]) + if (recipient.Item == item || recipient.Item == signal.source) { continue; } + + signal.source?.LastSentSignalRecipients.Add(recipient); + + foreach (ItemComponent ic in recipient.Item.Components) { - if (recipient.Item == item || recipient.Item == signal.source) { continue; } - - signal.source?.LastSentSignalRecipients.Add(recipient); - - foreach (ItemComponent ic in recipient.Item.Components) - { - //other junction boxes don't need to receive the signal in the pass-through signal connections - //because we relay it straight to the connected items without going through the whole chain of junction boxes - if (ic is PowerTransfer && !(ic is RelayComponent)) { continue; } - ic.ReceiveSignal(signal, recipient); - } + //other junction boxes don't need to receive the signal in the pass-through signal connections + //because we relay it straight to the connected items without going through the whole chain of junction boxes + if (ic is PowerTransfer && !(ic is RelayComponent)) { continue; } + ic.ReceiveSignal(signal, recipient); + } + if (recipient.Effects != null && signal.value != "0" && !string.IsNullOrEmpty(signal.value)) + { foreach (StatusEffect effect in recipient.Effects) { recipient.Item.ApplyStatusEffect(effect, ActionType.OnUse, 1.0f); } } - } + } } protected override void RemoveComponentSpecific() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs index 1db23da24..a5292d49c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs @@ -272,7 +272,7 @@ namespace Barotrauma.Items.Components powered.voltage = -pt1.CurrPowerConsumption / Math.Max(pt1.PowerLoad, 1.0f); continue; } - if (powered.powerConsumption <= 0.0f && !(powered is PowerContainer)) + if ((powered.powerConsumption <= 0.0f || (powered.Item.GetComponent() is Repairable repairable && repairable.IsTinkering && repairable.TinkeringPowersDevices)) && !(powered is PowerContainer)) { powered.voltage = 1.0f; continue; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index 9b1c13af8..aa31c365e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -268,7 +268,8 @@ namespace Barotrauma.Items.Components IgnoredBodies = ignoredBodies; Vector2 projectilePos = weaponPos; //make sure there's no obstacles between the base of the weapon (or the shoulder of the character) and the end of the barrel - if (Submarine.PickBody(weaponPos, spawnPos, IgnoredBodies, Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking) == null) + if (Submarine.PickBody(weaponPos, spawnPos, IgnoredBodies, Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking, + customPredicate: (Fixture f) => { return !IgnoredBodies.Contains(f.Body); }) == null) { //no obstacles -> we can spawn the projectile at the barrel projectilePos = spawnPos; @@ -359,7 +360,7 @@ namespace Barotrauma.Items.Components item.body.FarseerBody.IsBullet = true; item.body.CollisionCategories = Physics.CollisionProjectile; - item.body.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel; + item.body.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking; IsActive = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index 0952af1c7..2893f94f1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -16,6 +16,9 @@ namespace Barotrauma.Items.Components private float deteriorationTimer; private float deteriorateAlwaysResetTimer; + private int prevSentConditionValue; + private string conditionSignal; + bool wasBroken; bool wasGoodCondition; @@ -113,6 +116,9 @@ namespace Barotrauma.Items.Components public float TinkeringStrength => tinkeringStrength; + private bool tinkeringPowersDevices; + public bool TinkeringPowersDevices => tinkeringPowersDevices; + public bool IsBelowRepairThreshold => item.ConditionPercentage <= RepairThreshold; public bool IsBelowRepairIconThreshold => item.ConditionPercentage <= RepairThreshold / 2; @@ -266,6 +272,7 @@ namespace Barotrauma.Items.Components if (action == FixActions.Tinker) { tinkeringStrength = 1f + CurrentFixer.GetStatValue(StatTypes.TinkeringStrength); + tinkeringPowersDevices = CurrentFixer.HasAbilityFlag(AbilityFlags.TinkeringPowersDevices); if (character.HasAbilityFlag(AbilityFlags.CanTinkerFabricatorsAndDeconstructors) && item.GetComponent() != null || item.GetComponent() != null) { @@ -346,7 +353,13 @@ namespace Barotrauma.Items.Components UpdateProjSpecific(deltaTime); IsTinkering = false; - item.SendSignal($"{(int) item.ConditionPercentage}", "condition_out"); + if (prevSentConditionValue != (int)item.ConditionPercentage || conditionSignal == null) + { + prevSentConditionValue = (int)item.ConditionPercentage; + conditionSignal = prevSentConditionValue.ToString(); + } + + item.SendSignal(conditionSignal, "condition_out"); if (CurrentFixer == null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs index d54ec8b92..8cc329fce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/AndComponent.cs @@ -12,6 +12,8 @@ namespace Barotrauma.Items.Components //the output is sent if both inputs have received a signal within the timeframe protected float timeFrame; + + protected readonly Character[] signalSender = new Character[2]; [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 @@ -80,14 +82,14 @@ namespace Barotrauma.Items.Components bool sendOutput = true; for (int i = 0; i < timeSinceReceived.Length; i++) { - if (timeSinceReceived[i] > timeFrame) sendOutput = false; + if (timeSinceReceived[i] > timeFrame) { sendOutput = false; } timeSinceReceived[i] += deltaTime; } string signalOut = sendOutput ? output : falseOutput; - if (string.IsNullOrEmpty(signalOut)) return; + if (string.IsNullOrEmpty(signalOut)) { return; } - item.SendSignal(signalOut, "signal_out"); + item.SendSignal(new Signal(signalOut, sender: signalSender[0] ?? signalSender[1]), "signal_out"); } public override void ReceiveSignal(Signal signal, Connection connection) @@ -95,12 +97,14 @@ namespace Barotrauma.Items.Components switch (connection.Name) { case "signal_in1": - if (signal.value == "0") return; + if (signal.value == "0") { return; } timeSinceReceived[0] = 0.0f; + signalSender[0] = signal.sender; break; case "signal_in2": - if (signal.value == "0") return; + if (signal.value == "0") { return; } timeSinceReceived[1] = 0.0f; + signalSender[1] = signal.sender; break; case "set_output": output = signal.value; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs index 88883ccca..a6b340070 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ButtonTerminal.cs @@ -19,7 +19,7 @@ namespace Barotrauma.Items.Components private HashSet ActivatingItemPrefabs { get; set; } = new HashSet(); - private bool AllowUsingButtons => ActivatingItemPrefabs.None() || Container.Inventory.AllItems.Any(i => i != null && ActivatingItemPrefabs.Any(p => p == i.Prefab)); + private bool AllowUsingButtons => ActivatingItemPrefabs.None() || (Container != null && Container.Inventory.AllItems.Any(i => i != null && ActivatingItemPrefabs.Any(p => p == i.Prefab))); public ButtonTerminal(Item item, XElement element) : base(item, element) { @@ -101,12 +101,12 @@ namespace Barotrauma.Items.Components partial void OnItemLoadedProjSpecific(); - private bool SendSignal(int signalIndex, bool isServerMessage = false) + private bool SendSignal(int signalIndex, Character sender, bool isServerMessage = false) { if (!isServerMessage && !AllowUsingButtons) { return false; } string signal = Signals[signalIndex]; string connectionName = $"signal_out{signalIndex + 1}"; - item.SendSignal(signal, connectionName); + item.SendSignal(new Signal(signal, sender: sender), connectionName); return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConcatComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConcatComponent.cs index 4870ffd67..afcf91f2d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConcatComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConcatComponent.cs @@ -17,6 +17,12 @@ namespace Barotrauma.Items.Components } } + [Editable, Serialize("", false)] + public string Separator + { + get; + set; + } public ConcatComponent(Item item, XElement element) : base(item, element) @@ -25,7 +31,15 @@ namespace Barotrauma.Items.Components protected override string Calculate(string signal1, string signal2) { - string output = signal1 + signal2; + string output; + if (string.IsNullOrEmpty(Separator)) + { + output = signal1 + signal2; + } + else + { + output = signal1 + Separator + signal2; + } return output.Length <= maxOutputLength ? output : output.Substring(0, MaxOutputLength); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs index 90db70255..5119cb9a7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs @@ -25,7 +25,7 @@ namespace Barotrauma.Items.Components get { return wires; } } - private Item item; + private readonly Item item; public readonly bool IsOutput; @@ -142,7 +142,6 @@ namespace Barotrauma.Items.Components IsPower = Name == "power_in" || Name == "power" || Name == "power_out"; - Effects = new List(); wireId = new ushort[MaxWires]; @@ -164,6 +163,7 @@ namespace Barotrauma.Items.Components break; case "statuseffect": + Effects ??= new List(); Effects.Add(StatusEffect.Load(subElement, item.Name + ", connection " + Name)); break; } @@ -272,7 +272,7 @@ namespace Barotrauma.Items.Components ic.ReceiveSignal(signal, connection); } - if (signal.value != "0") + if (recipient.Effects != null && signal.value != "0" && !string.IsNullOrEmpty(signal.value)) { foreach (StatusEffect effect in recipient.Effects) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs index 79bc7f6a9..92103f633 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs @@ -24,7 +24,19 @@ namespace Barotrauma.Items.Components /// public bool AlwaysAllowRewiring { - get { return item.Submarine?.Info.Type == SubmarineType.BeaconStation; } + get + { + if (item.Submarine == null) { return true; } + switch (item.Submarine.Info.Type) + { + case SubmarineType.Wreck: + case SubmarineType.BeaconStation: + case SubmarineType.EnemySubmarine: + case SubmarineType.Ruin: + return true; + } + return false; + } } [Editable, Serialize(false, true, description: "Locked connection panels cannot be rewired in-game.", alwaysUseInstanceValues: true)] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs index bc7f71527..9dc5b92af 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs @@ -301,7 +301,6 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - UpdateProjSpecific(); foreach (CustomInterfaceElement ciElement in customInterfaceElementList) { if (!ciElement.ContinuousSignal) { continue; } @@ -318,8 +317,6 @@ namespace Barotrauma.Items.Components } } - partial void UpdateProjSpecific(); - public override XElement Save(XElement parentElement) { labels = customInterfaceElementList.Select(ci => ci.Label).ToArray(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs index 3249973db..b3bdf5f2c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/DelayComponent.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Xml.Linq; using Microsoft.Xna.Framework; namespace Barotrauma.Items.Components @@ -24,7 +25,7 @@ namespace Barotrauma.Items.Components private int signalQueueSize; private int delayTicks; - private readonly Queue signalQueue; + private readonly Queue signalQueue = new Queue(); private DelayedSignal prevQueuedSignal; @@ -39,6 +40,7 @@ namespace Barotrauma.Items.Components delay = value; delayTicks = (int)(delay / Timing.Step); signalQueueSize = Math.Max(delayTicks, 1) * 2; + signalQueue.Clear(); } } @@ -59,7 +61,6 @@ namespace Barotrauma.Items.Components public DelayComponent(Item item, XElement element) : base (item, element) { - signalQueue = new Queue(); IsActive = true; } @@ -74,7 +75,7 @@ namespace Barotrauma.Items.Components { var signalOut = signalQueue.Peek(); signalOut.SendDuration -= 1; - item.SendSignal(new Signal(signalOut.Signal.value, strength: signalOut.Signal.strength), "signal_out"); + item.SendSignal(new Signal(signalOut.Signal.value, sender: signalOut.Signal.sender, strength: signalOut.Signal.strength), "signal_out"); if (signalOut.SendDuration <= 0) { signalQueue.Dequeue(); @@ -115,7 +116,7 @@ namespace Barotrauma.Items.Components signalQueue.Enqueue(prevQueuedSignal); break; case "set_delay": - if (float.TryParse(signal.value, out float newDelay)) + if (float.TryParse(signal.value, NumberStyles.Any, CultureInfo.InvariantCulture, out float newDelay)) { newDelay = MathHelper.Clamp(newDelay, 0, 60); if (signalQueue.Count > 0 && newDelay != Delay) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs index 19175aaf3..4ffc063f7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/EqualsComponent.cs @@ -12,6 +12,8 @@ namespace Barotrauma.Items.Components protected string[] receivedSignal; + private readonly Character[] signalSender = new Character[2]; + //the output is sent if both inputs have received a signal within the timeframe protected float timeFrame; @@ -90,9 +92,8 @@ namespace Barotrauma.Items.Components if (sendOutput) { string signalOut = receivedSignal[0] == receivedSignal[1] ? output : falseOutput; - if (string.IsNullOrEmpty(signalOut)) return; - - item.SendSignal(signalOut, "signal_out"); + if (string.IsNullOrEmpty(signalOut)) { return; } + item.SendSignal(new Signal(signalOut, sender: signalSender[0] ?? signalSender[1]), "signal_out"); } } @@ -103,10 +104,15 @@ namespace Barotrauma.Items.Components case "signal_in1": receivedSignal[0] = signal.value; timeSinceReceived[0] = 0.0f; + signalSender[0] = signal.sender; break; case "signal_in2": receivedSignal[1] = signal.value; timeSinceReceived[1] = 0.0f; + signalSender[1] = signal.sender; + break; + case "set_output": + output = signal.value; break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/GreaterComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/GreaterComponent.cs index fa8109691..0f15476c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/GreaterComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/GreaterComponent.cs @@ -32,10 +32,22 @@ namespace Barotrauma.Items.Components } public override void ReceiveSignal(Signal signal, Connection connection) - { - base.ReceiveSignal(signal, connection); - float.TryParse(receivedSignal[0], NumberStyles.Float, CultureInfo.InvariantCulture, out val1); - float.TryParse(receivedSignal[1], NumberStyles.Float, CultureInfo.InvariantCulture, out val2); + { + //base.ReceiveSignal(signal, connection); + switch (connection.Name) + { + case "signal_in1": + float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out val1); + timeSinceReceived[0] = 0.0f; + break; + case "signal_in2": + float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out val2); + timeSinceReceived[1] = 0.0f; + break; + case "set_output": + output = signal.value; + break; + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index 876e785e3..e74aaff0d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -50,7 +50,7 @@ namespace Barotrauma.Items.Components set { rotation = value; - SetLightSourceTransform(); + SetLightSourceTransformProjSpecific(); } } @@ -256,7 +256,7 @@ namespace Barotrauma.Items.Components return; } - SetLightSourceTransform(); + SetLightSourceTransformProjSpecific(); PhysicsBody body = ParentBody ?? item.body; if (body != null && !body.Enabled) @@ -338,7 +338,11 @@ namespace Barotrauma.Items.Components partial void SetLightSourceState(bool enabled, float brightness); - partial void SetLightSourceTransform(); + public void SetLightSourceTransform() + { + SetLightSourceTransformProjSpecific(); + } + partial void SetLightSourceTransformProjSpecific(); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs index 14abecf89..3c4db92a5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs @@ -74,6 +74,17 @@ namespace Barotrauma.Items.Components } } + public Vector2 TransformedDetectOffset + { + get + { + Vector2 transformedDetectOffset = detectOffset; + if (item.FlippedX) { transformedDetectOffset.X = -transformedDetectOffset.X; } + if (item.FlippedY) { transformedDetectOffset.Y = -transformedDetectOffset.Y; } + return transformedDetectOffset; + } + } + [Editable(MinValueFloat = 0.1f, MaxValueFloat = 100.0f, DecimalCount = 2), Serialize(0.1f, true, description: "How often the sensor checks if there's something moving near it. Higher values are better for performance.", alwaysUseInstanceValues: true)] public float UpdateInterval { @@ -184,15 +195,15 @@ namespace Barotrauma.Items.Components } } - Vector2 detectPos = item.WorldPosition + detectOffset; + Vector2 detectPos = item.WorldPosition + TransformedDetectOffset; Rectangle detectRect = new Rectangle((int)(detectPos.X - rangeX), (int)(detectPos.Y - rangeY), (int)(rangeX * 2), (int)(rangeY * 2)); float broadRangeX = Math.Max(rangeX * 2, 500); float broadRangeY = Math.Max(rangeY * 2, 500); - if (item.CurrentHull == null && item.Submarine != null && Level.Loaded != null && + if (item.CurrentHull == null && item.Submarine != null && (Target == TargetType.Wall || Target == TargetType.Any)) { - if (Math.Abs(item.Submarine.Velocity.X) > MinimumVelocity || Math.Abs(item.Submarine.Velocity.Y) > MinimumVelocity) + if (Level.Loaded != null && (Math.Abs(item.Submarine.Velocity.X) > MinimumVelocity || Math.Abs(item.Submarine.Velocity.Y) > MinimumVelocity)) { var cells = Level.Loaded.GetCells(item.WorldPosition, 1); foreach (var cell in cells) @@ -268,7 +279,7 @@ namespace Barotrauma.Items.Components foreach (Limb limb in c.AnimController.Limbs) { if (limb.IsSevered) { continue; } - if (limb.LinearVelocity.LengthSquared() <= MinimumVelocity * MinimumVelocity) { continue; } + if (limb.LinearVelocity.LengthSquared() < MinimumVelocity * MinimumVelocity) { continue; } if (MathUtils.CircleIntersectsRectangle(limb.WorldPosition, ConvertUnits.ToDisplayUnits(limb.body.GetMaxExtent()), detectRect)) { MotionDetected = true; @@ -276,23 +287,12 @@ namespace Barotrauma.Items.Components } } } - } + } } - public override void FlipX(bool relativeToSub) - { - detectOffset.X = -detectOffset.X; - } - public override void FlipY(bool relativeToSub) - { - detectOffset.Y = -detectOffset.Y; - } public override XElement Save(XElement parentElement) { Vector2 prevDetectOffset = detectOffset; - //undo flipping before saving - if (item.FlippedX) { detectOffset.X = -detectOffset.X; } - if (item.FlippedY) { detectOffset.Y = -detectOffset.Y; } XElement element = base.Save(parentElement); detectOffset = prevDetectOffset; return element; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OrComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OrComponent.cs index d0661dc9c..b5c242653 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OrComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OrComponent.cs @@ -15,14 +15,14 @@ namespace Barotrauma.Items.Components bool sendOutput = false; for (int i = 0; i < timeSinceReceived.Length; i++) { - if (timeSinceReceived[i] <= timeFrame) sendOutput = true; + if (timeSinceReceived[i] <= timeFrame) { sendOutput = true; } timeSinceReceived[i] += deltaTime; } string signalOut = sendOutput ? output : falseOutput; - if (string.IsNullOrEmpty(signalOut)) return; + if (string.IsNullOrEmpty(signalOut)) { return; } - item.SendSignal(signalOut, "signal_out"); + item.SendSignal(new Signal(signalOut, sender: signalSender[0] ?? signalSender[1]), "signal_out"); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OxygenDetector.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OxygenDetector.cs index 4724dd56c..9cc1020e7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OxygenDetector.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/OxygenDetector.cs @@ -4,6 +4,9 @@ namespace Barotrauma.Items.Components { class OxygenDetector : ItemComponent { + private int prevSentOxygenValue; + private string oxygenSignal; + public OxygenDetector(Item item, XElement element) : base (item, element) { @@ -12,9 +15,15 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - if (item.CurrentHull == null) return; + if (item.CurrentHull == null) { return; } - item.SendSignal(((int)item.CurrentHull.OxygenPercentage).ToString(), "signal_out"); + if (prevSentOxygenValue != (int)item.CurrentHull.OxygenPercentage || oxygenSignal == null) + { + prevSentOxygenValue = (int)item.CurrentHull.OxygenPercentage; + oxygenSignal = prevSentOxygenValue.ToString(); + } + + item.SendSignal(oxygenSignal, "signal_out"); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs index 629d07c37..6d78cea05 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WaterDetector.cs @@ -9,6 +9,9 @@ namespace Barotrauma.Items.Components //how often the detector can switch from state to another const float StateSwitchInterval = 1.0f; + private int prevSentWaterPercentageValue; + private string waterPercentageSignal; + private bool isInWater; private float stateSwitchDelay; @@ -106,7 +109,12 @@ namespace Barotrauma.Items.Components { waterPercentage = MathHelper.Clamp((int)Math.Ceiling(item.CurrentHull.WaterPercentage), 0, 100); } - item.SendSignal(waterPercentage.ToString(), "water_%"); + if (prevSentWaterPercentageValue != waterPercentage || waterPercentageSignal == null) + { + prevSentWaterPercentageValue = waterPercentage; + waterPercentageSignal = prevSentWaterPercentageValue.ToString(); + } + item.SendSignal(waterPercentageSignal, "water_%"); } string highPressureOut = (item.CurrentHull == null || item.CurrentHull.LethalPressure > 5.0f) ? "1" : "0"; item.SendSignal(highPressureOut, "high_pressure"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs index dc5194e40..8f3b6cebb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs @@ -24,6 +24,7 @@ namespace Barotrauma.Items.Components private readonly int[] channelMemory = new int[ChannelMemorySize]; + private Connection signalInConnection; private Connection signalOutConnection; [Serialize(CharacterTeamType.None, true, description: "WiFi components can only communicate with components that have the same Team ID.", alwaysUseInstanceValues: true)] @@ -98,6 +99,7 @@ namespace Barotrauma.Items.Components if (item.Connections != null) { signalOutConnection = item.Connections.Find(c => c.Name == "signal_out"); + signalInConnection = item.Connections.Find(c => c.Name == "signal_in"); } if (channelMemory.All(m => m == 0)) { @@ -207,6 +209,18 @@ namespace Barotrauma.Items.Components if (wifiComp.signalOutConnection != null) { + if (signal.source != null && wifiComp.signalInConnection != null) + { + if (signal.source.LastSentSignalRecipients.Contains(wifiComp.signalInConnection)) + { + //signal already passed through this wifi component -> stop here to prevent an infinite loop + continue; + } + else + { + signal.source.LastSentSignalRecipients.Add(wifiComp.signalInConnection); + } + } wifiComp.item.SendSignal(s, wifiComp.signalOutConnection); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs index a3dae763f..ec7a0db91 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs @@ -503,13 +503,6 @@ namespace Barotrauma.Items.Components return true; } - public override void Move(Vector2 amount) - { -#if CLIENT - if (item.IsSelected) MoveNodes(amount); -#endif - } - public List GetNodes() { return new List(nodes); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/XorComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/XorComponent.cs index 134b53130..d608e046a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/XorComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/XorComponent.cs @@ -15,14 +15,14 @@ namespace Barotrauma.Items.Components int sendOutput = 0; for (int i = 0; i < timeSinceReceived.Length; i++) { - if (timeSinceReceived[i] <= timeFrame) sendOutput += 1; + if (timeSinceReceived[i] <= timeFrame) { sendOutput += 1; } timeSinceReceived[i] += deltaTime; } string signalOut = sendOutput == 1 ? output : falseOutput; - if (string.IsNullOrEmpty(signalOut)) return; + if (string.IsNullOrEmpty(signalOut)) { return; } - item.SendSignal(signalOut, "signal_out"); + item.SendSignal(new Signal(signalOut, sender: signalSender[0] ?? signalSender[1]), "signal_out"); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs index b7ff7d8bb..a748459c7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/TriggerComponent.cs @@ -141,8 +141,7 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { - triggerers.RemoveWhere(t => t.Removed); - LevelTrigger.RemoveDistantTriggerers(PhysicsBody, triggerers, item.WorldPosition); + LevelTrigger.RemoveInActiveTriggerers(PhysicsBody, triggerers); if (triggerOnce) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index fdaca5d18..f63c0dc9d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -333,6 +333,7 @@ namespace Barotrauma.Items.Components FindLightComponent(); if (loadedRotationLimits.HasValue) { RotationLimits = loadedRotationLimits.Value; } if (loadedBaseRotation.HasValue) { BaseRotation = loadedBaseRotation.Value; } + targetRotation = rotation; UpdateTransformedBarrelPos(); } @@ -1516,7 +1517,7 @@ namespace Barotrauma.Items.Components minRotation += MathHelper.TwoPi; maxRotation += MathHelper.TwoPi; } - rotation = (minRotation + maxRotation) / 2; + targetRotation = rotation = (minRotation + maxRotation) / 2; UpdateTransformedBarrelPos(); } @@ -1537,7 +1538,7 @@ namespace Barotrauma.Items.Components minRotation += MathHelper.TwoPi; maxRotation += MathHelper.TwoPi; } - rotation = (minRotation + maxRotation) / 2; + targetRotation = rotation = (minRotation + maxRotation) / 2; UpdateTransformedBarrelPos(); } @@ -1607,6 +1608,7 @@ namespace Barotrauma.Items.Components { base.OnItemLoaded(); FindLightComponent(); + targetRotation = rotation; if (!loadedBaseRotation.HasValue) { if (item.FlippedX) { FlipX(relativeToSub: false); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index dbfdcf3b7..fbfb7a2ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -479,8 +479,11 @@ namespace Barotrauma { if (i < 0 || i >= slots.Length) { - string errorMsg = "Inventory.TryPutItem failed: index was out of range(" + i + ").\n" + Environment.StackTrace.CleanupStackTrace(); + string errorMsg = $"Inventory.TryPutItem failed: index was out of range (item: {(item?.Name ?? "null")}, inventory: {(Owner?.ToString() ?? "null")}).\n" + Environment.StackTrace.CleanupStackTrace(); GameAnalyticsManager.AddErrorEventOnce("Inventory.TryPutItem:IndexOutOfRange", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); +#if DEBUG + DebugConsole.ThrowError(errorMsg); +#endif return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 868912104..872ae1adf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -262,6 +262,7 @@ namespace Barotrauma if (Screen.Selected == GameMain.SubEditorScreen) { SetContainedItemPositions(); + GetComponent()?.SetLightSourceTransform(); } #endif } @@ -732,12 +733,12 @@ namespace Barotrauma "Disclaimer: It's possible or even likely that the views block each other, if they were not designed to be viewed together!")] public bool DisplaySideBySideWhenLinked { get; set; } - public IEnumerable Repairables + public List Repairables { get { return repairables; } } - public IEnumerable Components + public List Components { get { return components; } } @@ -782,7 +783,7 @@ namespace Barotrauma } private readonly List allPropertyObjects = new List(); - public IEnumerable AllPropertyObjects + public IReadOnlyList AllPropertyObjects { get { return allPropertyObjects; } } @@ -1079,7 +1080,7 @@ namespace Barotrauma allPropertyObjects.Add(component); components.Add(component); - if (component.IsActive || component.Parent != null || (component.IsActiveConditionals != null && component.IsActiveConditionals.Any())) + if (component.IsActive || component.UpdateWhenInactive || component.Parent != null || (component.IsActiveConditionals != null && component.IsActiveConditionals.Any())) { updateableComponents.Add(component); } @@ -1092,7 +1093,7 @@ namespace Barotrauma #endif //component doesn't need to be updated if it isn't active, doesn't have a parent that could activate it, //nor status effects, sounds or conditionals that would need to run - if (!isActive && + if (!isActive && !component.UpdateWhenInactive && !hasSounds && component.Parent == null && (component.IsActiveConditionals == null || !component.IsActiveConditionals.Any()) && @@ -1672,7 +1673,7 @@ namespace Barotrauma ic.WasUsed = false; ic.WasSecondaryUsed = false; - if (ic.IsActive) + if (ic.IsActive || ic.UpdateWhenInactive) { if (condition <= 0.0f) { @@ -1809,7 +1810,7 @@ namespace Barotrauma /// private void ApplyWaterForces() { - if (body.Mass <= 0.0f) + if (body.Mass <= 0.0f || body.Density <= 0.0f) { return; } @@ -2087,18 +2088,19 @@ namespace Barotrauma return controller != null; } - public void SendSignal(string signal, string connectionName) + public bool SendSignal(string signal, string connectionName) { - SendSignal(new Signal(signal), connectionName); + return SendSignal(new Signal(signal), connectionName); } - public void SendSignal(Signal signal, string connectionName) + public bool SendSignal(Signal signal, string connectionName) { - if (connections == null) { return; } - if (!connections.TryGetValue(connectionName, out Connection connection)) { return; } + if (connections == null) { return false; } + if (!connections.TryGetValue(connectionName, out Connection connection)) { return false; } signal.source ??= this; SendSignal(signal, connection); + return true; } private readonly HashSet<(Signal Signal, Connection Connection)> delayedSignals = new HashSet<(Signal Signal, Connection Connection)>(); @@ -2113,9 +2115,14 @@ namespace Barotrauma //if the signal has been passed through this item multiple times already, interrupt it to prevent infinite loops if (signal.stepsTaken > 5 && signal.source != null) { - if (signal.source.LastSentSignalRecipients.AtLeast(3, recipient => recipient == connection)) + int duplicateRecipients = 0; + foreach (var recipient in signal.source.LastSentSignalRecipients) { - return; + if (recipient == connection) + { + duplicateRecipients++; + if (duplicateRecipients > 2) { return; } + } } } @@ -2126,7 +2133,16 @@ namespace Barotrauma //if there's an equal signal waiting to be sent //to the same connection, don't add a new one signal.stepsTaken = 0; - if (!delayedSignals.Any(s => s.Connection == connection && s.Signal.source == signal.source && s.Signal.value == signal.value && s.Signal.sender == signal.sender)) + bool duplicateFound = false; + foreach (var s in delayedSignals) + { + if (s.Connection == connection && s.Signal.source == signal.source && s.Signal.value == signal.value && s.Signal.sender == signal.sender) + { + duplicateFound = true; + break; + } + } + if (!duplicateFound) { delayedSignals.Add((signal, connection)); CoroutineManager.StartCoroutine(DelaySignal(signal, connection)); @@ -2134,16 +2150,17 @@ namespace Barotrauma } else { - foreach (StatusEffect effect in connection.Effects) + if (connection.Effects != null && signal.value != "0" && !string.IsNullOrEmpty(signal.value)) { - if (condition <= 0.0f && effect.type != ActionType.OnBroken) { continue; } - if (signal.value != "0" && !string.IsNullOrEmpty(signal.value)) { ApplyStatusEffect(effect, ActionType.OnUse, (float)Timing.Step); } + foreach (StatusEffect effect in connection.Effects) + { + if (condition <= 0.0f && effect.type != ActionType.OnBroken) { continue; } + ApplyStatusEffect(effect, ActionType.OnUse, (float)Timing.Step); + } } - signal.source ??= this; connection.SendSignal(signal); } - } private IEnumerable DelaySignal(Signal signal, Connection connection) @@ -2464,8 +2481,6 @@ namespace Barotrauma } - - if (remove) { Spawner?.AddToRemoveQueue(this); } } @@ -2850,6 +2865,14 @@ namespace Barotrauma string name = element.Attribute("name").Value; string identifier = element.GetAttributeString("identifier", ""); + if (string.IsNullOrWhiteSpace(name) && string.IsNullOrWhiteSpace(identifier)) + { + string errorMessage = "Failed to load an item (both name and identifier were null):\n"+element.ToString(); + DebugConsole.ThrowError(errorMessage); + GameAnalyticsManager.AddErrorEventOnce("Item.Load:NameAndIdentifierNull", GameAnalyticsManager.ErrorSeverity.Error, errorMessage); + return null; + } + string pendingSwap = element.GetAttributeString("pendingswap", ""); ItemPrefab appliedSwap = null; ItemPrefab oldPrefab = null; @@ -3274,4 +3297,17 @@ namespace Barotrauma } } } + class AbilityApplyTreatment : AbilityObject, IAbilityCharacter, IAbilityItem + { + public Character Character { get; set; } + public Character User { get; set; } + public Item Item { get; set; } + + public AbilityApplyTreatment(Character user, Character target, Item item) + { + Character = target; + User = user; + Item = item; + } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index dfec46927..c822cffe3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -105,6 +105,10 @@ namespace Barotrauma RequiredSkills = new List(); RequiredTime = element.GetAttributeFloat("requiredtime", 1.0f); OutCondition = element.GetAttributeFloat("outcondition", 1.0f); + if (OutCondition > 1.0f) + { + DebugConsole.AddWarning($"Error in \"{itemPrefab.Name}\"'s fabrication recipe: out condition is above 100% ({OutCondition * 100})."); + } RequiredItems = new List(); RequiresRecipe = element.GetAttributeBool("requiresrecipe", false); Amount = element.GetAttributeInt("amount", 1); @@ -1256,13 +1260,18 @@ namespace Barotrauma public static ItemPrefab Find(string name, string identifier) { + if (string.IsNullOrEmpty(name) && string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("Both name and identifier cannot be null."); + } + ItemPrefab prefab; if (string.IsNullOrEmpty(identifier)) { //legacy support identifier = GenerateLegacyIdentifier(name); } - prefab = Find(p => p is ItemPrefab && p.Identifier==identifier) as ItemPrefab; + prefab = Find(p => p is ItemPrefab && p.Identifier == identifier) as ItemPrefab; //not found, see if we can find a prefab with a matching alias if (prefab == null && !string.IsNullOrEmpty(name)) @@ -1478,5 +1487,10 @@ namespace Barotrauma return newElement; } + + public override string ToString() + { + return $"{Name} (identifier: {Identifier})"; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs index 710b21946..b4a905486 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs @@ -794,7 +794,7 @@ namespace Barotrauma.MapCreatures.Behavior if (parent != null) { - if (otherBranch.BlockedSides.IsBitSet(connectingSide)) + if (otherBranch.BlockedSides.HasFlag(connectingSide)) { branch.BlockedSides |= oppositeSide; continue; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index e91c25f7b..1c0e18576 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -68,10 +68,9 @@ namespace Barotrauma force = element.GetAttributeFloat("force", 0.0f); - abilityExplosion = element.GetAttributeBool("abilityexplosion", false); applyToSelf = element.GetAttributeBool("applytoself", true); - bool showEffects = !abilityExplosion; + bool showEffects = !element.GetAttributeBool("abilityexplosion", false) && element.GetAttributeBool("showeffects", true); sparks = element.GetAttributeBool("sparks", showEffects); shockwave = element.GetAttributeBool("shockwave", showEffects); flames = element.GetAttributeBool("flames", showEffects); @@ -151,7 +150,7 @@ namespace Barotrauma if (!MathUtils.NearlyEqual(Attack.GetStructureDamage(1.0f), 0.0f) || !MathUtils.NearlyEqual(Attack.GetLevelWallDamage(1.0f), 0.0f)) { - RangedStructureDamage(worldPosition, displayRange, Attack.GetStructureDamage(1.0f), Attack.GetLevelWallDamage(1.0f), attacker, IgnoredSubmarines); + RangedStructureDamage(worldPosition, displayRange, Attack.GetStructureDamage(1.0f), Attack.GetLevelWallDamage(1.0f), attacker, IgnoredSubmarines, Attack.EmitStructureDamageParticles); } if (BallastFloraDamage > 0.0f) @@ -388,7 +387,7 @@ namespace Barotrauma { if (damages.TryGetValue(limb, out float damage)) { - c.TrySeverLimbJoints(limb, attack.SeverLimbsProbability * distFactor, damage, allowBeheading: true); + c.TrySeverLimbJoints(limb, attack.SeverLimbsProbability * distFactor, damage, allowBeheading: true, attacker: attacker); } } } @@ -396,13 +395,15 @@ namespace Barotrauma } } + private static readonly List damagedStructureList = new List(); + private static readonly Dictionary damagedStructures = new Dictionary(); /// /// Returns a dictionary where the keys are the structures that took damage and the values are the amount of damage taken /// - public static Dictionary RangedStructureDamage(Vector2 worldPosition, float worldRange, float damage, float levelWallDamage, Character attacker = null, IEnumerable ignoredSubmarines = null) + public static Dictionary RangedStructureDamage(Vector2 worldPosition, float worldRange, float damage, float levelWallDamage, Character attacker = null, IEnumerable ignoredSubmarines = null, bool emitWallDamageParticles = true) { - List structureList = new List(); float dist = 600.0f; + damagedStructureList.Clear(); foreach (MapEntity entity in MapEntity.mapEntityList) { if (!(entity is Structure structure)) { continue; } @@ -412,19 +413,19 @@ namespace Barotrauma !structure.IsPlatform && Vector2.Distance(structure.WorldPosition, worldPosition) < dist * 3.0f) { - structureList.Add(structure); + damagedStructureList.Add(structure); } } - Dictionary damagedStructures = new Dictionary(); - foreach (Structure structure in structureList) + damagedStructures.Clear(); + foreach (Structure structure in damagedStructureList) { for (int i = 0; i < structure.SectionCount; i++) { float distFactor = 1.0f - (Vector2.Distance(structure.SectionPosition(i, true), worldPosition) / worldRange); if (distFactor <= 0.0f) { continue; } - structure.AddDamage(i, damage * distFactor, attacker); + structure.AddDamage(i, damage * distFactor, attacker, emitParticles: emitWallDamageParticles); if (damagedStructures.ContainsKey(structure)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 6993379c2..033dae146 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -1233,7 +1233,7 @@ namespace Barotrauma } } - Rectangle subRect = Submarine.CalculateDimensions(); + Rectangle subRect = Submarine.Borders; Alignment roomPos; if (rect.Y - rect.Height / 2 > subRect.Y + subRect.Height * 0.66f) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 94b486cc2..2a888ef49 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -574,9 +574,9 @@ namespace Barotrauma siteCoordsX = new List((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y)); siteCoordsY = new List((borders.Height / siteInterval.Y) * (borders.Width / siteInterval.Y)); int caveSiteInterval = 500; - for (int x = siteInterval.X / 2; x < borders.Width; x += siteInterval.X) + for (int x = siteInterval.X / 2; x < borders.Width - siteInterval.X / 2; x += siteInterval.X) { - for (int y = siteInterval.Y / 2; y < borders.Height; y += siteInterval.Y) + for (int y = siteInterval.Y / 2; y < borders.Height - siteInterval.Y / 2; y += siteInterval.Y) { int siteX = x + Rand.Range(-siteVariance.X, siteVariance.X, Rand.RandSync.Server); int siteY = y + Rand.Range(-siteVariance.Y, siteVariance.Y, Rand.RandSync.Server); @@ -613,8 +613,11 @@ namespace Barotrauma if (Rand.Range(0, 10, Rand.RandSync.Server) != 0) { continue; } } - siteCoordsX.Add(siteX); - siteCoordsY.Add(siteY); + if (!TooClose(siteX, siteY)) + { + siteCoordsX.Add(siteX); + siteCoordsY.Add(siteY); + } if (closeToCave) { @@ -625,24 +628,45 @@ namespace Barotrauma int caveSiteX = x2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.Server); int caveSiteY = y2 + Rand.Int(caveSiteInterval / 2, Rand.RandSync.Server); - bool tooClose = false; - for (int i = 0; i < siteCoordsX.Count; i++) + if (!TooClose(caveSiteX, caveSiteY)) { - if (MathUtils.DistanceSquared(caveSiteX, caveSiteY, siteCoordsX[i], siteCoordsY[i]) < 10.0f * 10.0f) - { - tooClose = true; - break; - } + siteCoordsX.Add(caveSiteX); + siteCoordsY.Add(caveSiteY); } - if (tooClose) { continue; } - siteCoordsX.Add(caveSiteX); - siteCoordsY.Add(caveSiteY); } } } } } + bool TooClose(double siteX, double siteY) + { + for (int i = 0; i < siteCoordsX.Count; i++) + { + if (MathUtils.DistanceSquared(siteCoordsX[i], siteCoordsY[i], siteX, siteY) < 10.0f * 10.0f) + { + return true; + } + } + return false; + } + + for (int i = 0; i < siteCoordsX.Count; i++) + { + Debug.Assert( + siteCoordsX[i] > 0 || siteCoordsY[i] > 0, + $"Potential error in level generation: a voronoi site was outside the bounds of the level ({siteCoordsX[i]}, {siteCoordsY[i]})"); + Debug.Assert( + siteCoordsX[i] < borders.Width || siteCoordsY[i] < borders.Height, + $"Potential error in level generation: a voronoi site was outside the bounds of the level ({siteCoordsX[i]}, {siteCoordsY[i]})"); + for (int j = i + 1; j < siteCoordsX.Count; j++) + { + Debug.Assert( + MathUtils.DistanceSquared(siteCoordsX[i], siteCoordsY[i], siteCoordsX[j], siteCoordsY[j]) > 1.0f, + "Potential error in level generation: two voronoi sites are extremely close to each other."); + } + } + EqualityCheckValues.Add(Rand.Int(int.MaxValue, Rand.RandSync.Server)); //---------------------------------------------------------------------------------- @@ -1011,7 +1035,7 @@ namespace Barotrauma foreach (InterestingPosition pos in PositionsOfInterest) { if (pos.PositionType != PositionType.MainPath && pos.PositionType != PositionType.SidePath) { continue; } - if (pos.Position.X < 5000 || pos.Position.X > Size.X - 5000) { continue; } + if (pos.Position.X < pathBorders.X + minMainPathWidth || pos.Position.X > pathBorders.Right - minMainPathWidth) { continue; } if (Math.Abs(pos.Position.X - startPosition.X) < minMainPathWidth * 2 || Math.Abs(pos.Position.X - endPosition.X) < minMainPathWidth * 2) { continue; } if (GetTooCloseCells(pos.Position.ToVector2(), minMainPathWidth * 0.7f).Count > 0) { continue; } iceChunkPositions.Add(pos.Position); @@ -2897,6 +2921,7 @@ namespace Barotrauma float? edgeLength = null, float maxResourceOverlap = 0.4f) { edgeLength ??= Vector2.Distance(location.Edge.Point1, location.Edge.Point2); + Vector2 edgeDir = (location.Edge.Point2 - location.Edge.Point1) / edgeLength.Value; var minResourceOverlap = -((edgeLength.Value - (resourceCount * resourcePrefab.Size.X)) / (resourceCount * resourcePrefab.Size.X)); minResourceOverlap = Math.Max(minResourceOverlap, 0.0f); var lerpAmounts = new float[resourceCount]; @@ -2912,7 +2937,7 @@ namespace Barotrauma placedResources = new List(); for (int i = 0; i < resourceCount; i++) { - Vector2 selectedPos = Vector2.Lerp(location.Edge.Point1, location.Edge.Point2, startOffset + lerpAmounts[i]); + Vector2 selectedPos = Vector2.Lerp(location.Edge.Point1 + edgeDir * resourcePrefab.Size.X / 2, location.Edge.Point2 - edgeDir * resourcePrefab.Size.X / 2, startOffset + lerpAmounts[i]); var item = new Item(resourcePrefab, selectedPos, submarine: null); Vector2 edgeNormal = location.Edge.GetNormal(location.Cell); float moveAmount = (item.body == null ? item.Rect.Height / 2 : ConvertUnits.ToDisplayUnits(item.body.GetMaxExtent() * 0.7f)); @@ -3627,10 +3652,23 @@ namespace Barotrauma Wrecks = new List(wreckCount); for (int i = 0; i < wreckCount; i++) { - ContentFile contentFile = wreckFiles[i]; - if (contentFile == null) { continue; } - string wreckName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path); - SpawnSubOnPath(wreckName, contentFile, SubmarineType.Wreck); + //how many times we'll try placing another sub before giving up + const int MaxSubsToTry = 2; + int attempts = 0; + while (wreckFiles.Any() && attempts < MaxSubsToTry) + { + ContentFile contentFile = wreckFiles.First(); + wreckFiles.RemoveAt(0); + if (contentFile == null) { continue; } + string wreckName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path); + if (SpawnSubOnPath(wreckName, contentFile, SubmarineType.Wreck) != null) + { + //placed successfully + break; + } + attempts++; + } + } totalSW.Stop(); Debug.WriteLine($"{Wrecks.Count} wrecks created in { totalSW.ElapsedMilliseconds} (ms)"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs index 87d9e6887..c6b2c0fb7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelTrigger.cs @@ -433,6 +433,31 @@ namespace Barotrauma return false; } + /// + /// Are there any active contacts between the physics body and the target entity + /// + public static bool CheckContactsForEntity(PhysicsBody triggerBody, Entity separatingEntity) + { + foreach (Fixture fixture in triggerBody.FarseerBody.FixtureList) + { + ContactEdge contactEdge = fixture.Body.ContactList; + while (contactEdge != null) + { + if (contactEdge.Contact != null && + contactEdge.Contact.Enabled && + contactEdge.Contact.IsTouching) + { + if (contactEdge.Contact.FixtureA != fixture && contactEdge.Contact.FixtureB != fixture) + { + if (GetEntity(contactEdge.Contact.FixtureB) == separatingEntity || GetEntity(contactEdge.Contact.FixtureA) == separatingEntity) { return true; } + } + } + contactEdge = contactEdge.Next; + } + } + return false; + } + public static Entity GetEntity(Fixture fixture) { if (fixture.Body == null || fixture.Body.UserData == null) { return null; } @@ -472,9 +497,6 @@ namespace Barotrauma { if (ParentTrigger != null && !ParentTrigger.IsTriggered) { return; } - triggerers.RemoveWhere(t => t.Removed); - - RemoveDistantTriggerers(PhysicsBody, triggerers, WorldPosition); bool isNotClient = true; #if CLIENT @@ -583,15 +605,27 @@ namespace Barotrauma } } - public static void RemoveDistantTriggerers(PhysicsBody physicsBody, HashSet triggerers, Vector2 calculateDistanceTo) + private static readonly List triggerersToRemove = new List(); + public static void RemoveInActiveTriggerers(PhysicsBody physicsBody, HashSet triggerers) { - //failsafe to ensure triggerers get removed when they're far from the trigger if (physicsBody == null) { return; } - float maxExtent = Math.Max(ConvertUnits.ToDisplayUnits(physicsBody.GetMaxExtent() * 5), 5000.0f); - triggerers.RemoveWhere(t => + + triggerersToRemove.Clear(); + foreach (var triggerer in triggerers) { - return Vector2.Distance(t.WorldPosition, calculateDistanceTo) > maxExtent; - }); + if (triggerer.Removed) + { + triggerersToRemove.Add(triggerer); + } + else if (!CheckContactsForEntity(physicsBody, triggerer)) + { + triggerersToRemove.Add(triggerer); + } + } + foreach (var triggerer in triggerersToRemove) + { + triggerers.Remove(triggerer); + } } public static void ApplyStatusEffects(List statusEffects, Vector2 worldPosition, Entity triggerer, float deltaTime, List targets) @@ -650,7 +684,7 @@ namespace Barotrauma float structureDamage = attack.GetStructureDamage(deltaTime); if (structureDamage > 0.0f) { - Explosion.RangedStructureDamage(worldPosition, attack.DamageRange, structureDamage, levelWallDamage: 0.0f); + Explosion.RangedStructureDamage(worldPosition, attack.DamageRange, structureDamage, levelWallDamage: 0.0f, emitWallDamageParticles: attack.EmitStructureDamageParticles); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index 7a8e2f397..b080d772b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -1,4 +1,5 @@ -using Barotrauma.Extensions; +using Barotrauma.Abilities; +using Barotrauma.Extensions; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -132,6 +133,7 @@ namespace Barotrauma #endregion private const float MechanicalMaxDiscountPercentage = 50.0f; + private const float HealMaxDiscountPercentage = 10.0f; private readonly List takenItems = new List(); public IEnumerable TakenItems @@ -908,6 +910,12 @@ namespace Barotrauma return (int) Math.Ceiling((1.0f - discount) * cost * MechanicalPriceMultiplier); } + public int GetAdjustedHealCost(int cost) + { + float discount = Reputation.Value / Reputation.MaxReputation * (HealMaxDiscountPercentage / 100.0f); + return (int) Math.Ceiling((1.0f - discount) * cost * PriceMultiplier); + } + /// If true, the store will be recreated if it already exists. public void CreateStore(bool force = false) { @@ -1110,7 +1118,7 @@ namespace Barotrauma Discovered = true; if (checkTalents) { - GameSession.GetSessionCrewCharacters().ForEach(c => c.CheckTalents(AbilityEffectType.OnLocationDiscovered, new Abilities.AbilityLocation(this))); + GameSession.GetSessionCrewCharacters().ForEach(c => c.CheckTalents(AbilityEffectType.OnLocationDiscovered, new AbilityLocation(this))); } } @@ -1263,5 +1271,15 @@ namespace Barotrauma { HireManager?.Remove(); } + + class AbilityLocation : AbilityObject, IAbilityLocation + { + public AbilityLocation(Location location) + { + Location = location; + } + + public Location Location { get; set; } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index c53c98b81..1683e5269 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -20,14 +20,14 @@ namespace Barotrauma protected List linkedToID; public List unresolvedLinkedToID; - + /// /// List of upgrades this item has /// protected readonly List Upgrades = new List(); - + public HashSet disallowedUpgrades = new HashSet(); - + [Editable, Serialize("", true)] public string DisallowedUpgrades { @@ -101,7 +101,7 @@ namespace Barotrauma return !DrawBelowWater; } } - + public virtual bool Linkable { get { return false; } @@ -231,6 +231,9 @@ namespace Barotrauma protected set; } = true; + [Serialize("", true, "Submarine editor layer")] + public string Layer { get; set; } + /// /// The index of the outpost module this entity originally spawned in (-1 if not an outpost item) /// @@ -242,7 +245,7 @@ namespace Barotrauma { get { return ""; } } - + public MapEntity(MapEntityPrefab prefab, Submarine submarine, ushort id) : base(submarine, id) { this.prefab = prefab; @@ -303,7 +306,7 @@ namespace Barotrauma { return GetUpgrade(identifier) != null; } - + public Upgrade GetUpgrade(string identifier) { return Upgrades.Find(upgrade => upgrade.Identifier == identifier); @@ -329,7 +332,7 @@ namespace Barotrauma } DebugConsole.Log($"Set (ID: {ID} {prefab.Name})'s \"{upgrade.Prefab.Name}\" upgrade to level {upgrade.Level}"); } - + /// /// Adds a new upgrade to the item /// @@ -435,7 +438,7 @@ namespace Barotrauma disconnectedFromClone.DisconnectedWires.Add(cloneWire); if (cloneWire.Item.body != null) { cloneWire.Item.body.Enabled = false; } cloneWire.IsActive = false; - continue; + continue; } var connectedItem = originalWire.Connections[n].Item; @@ -552,7 +555,7 @@ namespace Barotrauma } - //update gaps in random order, because otherwise in rooms with multiple gaps + //update gaps in random order, because otherwise in rooms with multiple gaps //the water/air will always tend to flow through the first gap in the list, //which may lead to weird behavior like water draining down only through //one gap in a room even if there are several @@ -725,11 +728,11 @@ namespace Barotrauma foreach (ushort i in e.linkedToID) { - if (FindEntityByID(i) is MapEntity linked) + if (FindEntityByID(i) is MapEntity linked) { - e.linkedTo.Add(linked); - } - else + e.linkedTo.Add(linked); + } + else { #if DEBUG DebugConsole.ThrowError($"Linking the entity \"{e.Name}\" to another entity failed. Could not find an entity with the ID \"{i}\"."); @@ -770,7 +773,7 @@ namespace Barotrauma /// /// Gets all linked entities of specific type. /// - private static void GetLinkedEntitiesRecursive(MapEntity mapEntity, HashSet linkedTargets, ref int depth, int? maxDepth = null, Func filter = null) + private static void GetLinkedEntitiesRecursive(MapEntity mapEntity, HashSet linkedTargets, ref int depth, int? maxDepth = null, Func filter = null) where T : MapEntity { if (depth > maxDepth) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs index 93eedb775..dd0dcc2b0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs @@ -338,6 +338,8 @@ namespace Barotrauma if (target == null) { return false; } if (target is StructurePrefab && AllowedLinks.Contains("structure")) { return true; } if (target is ItemPrefab && AllowedLinks.Contains("item")) { return true; } + if (target is LinkedSubmarinePrefab && Tags.Contains("dock")) { return true; } + if (this is LinkedSubmarinePrefab && target.Tags.Contains("dock")) { return true; } return AllowedLinks.Contains(target.Identifier) || target.AllowedLinks.Contains(identifier) || target.Tags.Any(t => AllowedLinks.Contains(t)) || Tags.Any(t => target.AllowedLinks.Contains(t)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index 9169f06e4..322b98b77 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -115,7 +115,7 @@ namespace Barotrauma private float? maxHealth; - [Serialize(100.0f, true)] + [Serialize(100.0f, true), Editable] public float MaxHealth { get => maxHealth ?? Prefab.Health; @@ -704,7 +704,7 @@ namespace Barotrauma if (BodyWidth > 0.0f) { rectSize.X = BodyWidth; } if (BodyHeight > 0.0f) { rectSize.Y = BodyHeight; } - Vector2 bodyPos = WorldPosition + BodyOffset; + Vector2 bodyPos = WorldPosition + BodyOffset * Scale; Vector2 transformedMousePos = MathUtils.RotatePointAroundTarget(position, bodyPos, BodyRotation); @@ -876,7 +876,7 @@ namespace Barotrauma return true; } - public void AddDamage(int sectionIndex, float damage, Character attacker = null) + public void AddDamage(int sectionIndex, float damage, Character attacker = null, bool emitParticles = true) { if (!Prefab.Body || Prefab.Platform || Indestructible) { return; } @@ -885,7 +885,7 @@ namespace Barotrauma var section = Sections[sectionIndex]; #if CLIENT - if (damage > 0) + if (damage > 0 && emitParticles) { float dmg = Math.Min(MaxHealth - section.damage, damage); float particleAmount = MathHelper.Lerp(0, 25, MathUtils.InverseLerp(0, 100, dmg * Rand.Range(0.75f, 1.25f))); @@ -1016,7 +1016,10 @@ namespace Barotrauma damageAmount = attack.GetStructureDamage(deltaTime); AddDamage(i, damageAmount, attacker); #if CLIENT - GameMain.ParticleManager.CreateParticle("dustcloud", SectionPosition(i), 0.0f, 0.0f); + if (attack.EmitStructureDamageParticles) + { + GameMain.ParticleManager.CreateParticle("dustcloud", SectionPosition(i), 0.0f, 0.0f); + } #endif } } @@ -1034,7 +1037,7 @@ namespace Barotrauma if (Submarine != null && damageAmount > 0 && attacker != null) { - var abilityAttackerSubmarine = new AbilityCharacterSubmarine(attacker, Submarine); + var abilityAttackerSubmarine = new AbilityAttackerSubmarine(attacker, Submarine); foreach (Character character in Character.CharacterList) { character.CheckTalents(AbilityEffectType.AfterSubmarineAttacked, abilityAttackerSubmarine); @@ -1529,6 +1532,7 @@ namespace Barotrauma public virtual void Reset() { SerializableProperties = SerializableProperty.DeserializeProperties(this, Prefab.ConfigElement); + MaxHealth = Prefab.Health; Sprite.ReloadXML(); SpriteDepth = Sprite.Depth; NoAITarget = Prefab.NoAITarget; @@ -1542,4 +1546,15 @@ namespace Barotrauma } } } + + class AbilityAttackerSubmarine : AbilityObject, IAbilityCharacter, IAbilitySubmarine + { + public AbilityAttackerSubmarine(Character character, Submarine submarine) + { + Character = character; + Submarine = submarine; + } + public Character Character { get; set; } + public Submarine Submarine { get; set; } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index 9dad1fd8d..efec856ac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -1535,7 +1535,7 @@ namespace Barotrauma element.Add(new XAttribute("tags", Info.Tags.ToString())); element.Add(new XAttribute("gameversion", GameMain.Version.ToString())); - Rectangle dimensions = CalculateDimensions(); + Rectangle dimensions = VisibleBorders; element.Add(new XAttribute("dimensions", XMLExtensions.Vector2ToString(dimensions.Size.ToVector2()))); var cargoContainers = GetCargoContainers(); element.Add(new XAttribute("cargocapacity", cargoContainers.Sum(c => c.container.Capacity))); @@ -1615,7 +1615,7 @@ namespace Barotrauma Info.CheckSubsLeftBehind(element); } - public bool SaveAs(string filePath, System.IO.MemoryStream previewImage = null) + public bool TrySaveAs(string filePath, System.IO.MemoryStream previewImage = null) { var newInfo = new SubmarineInfo(this) { @@ -1628,8 +1628,19 @@ namespace Barotrauma //remove reference to the preview image from the old info, so we don't dispose it (the new info still uses the texture) Info.PreviewImage = null; #endif - Info.Dispose(); Info = newInfo; - return newInfo.SaveAs(filePath, previewImage); + Info.Dispose(); + Info = newInfo; + + try + { + newInfo.SaveAs(filePath, previewImage); + } + catch (Exception e) + { + DebugConsole.ThrowError($"Saving submarine \"{filePath}\" failed!", e); + return false; + } + return true; } public static bool Unloading @@ -1643,9 +1654,8 @@ namespace Barotrauma Unloading = true; #if CLIENT - RemoveAllRoundSounds(); //Sound.OnGameEnd(); - - if (GameMain.LightManager != null) GameMain.LightManager.ClearLights(); + RemoveAllRoundSounds(); + GameMain.LightManager?.ClearLights(); #endif var _loaded = new List(loaded); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index 913d9e2cf..499acf25d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -115,6 +115,8 @@ namespace Barotrauma { this.submarine = sub; + Vector2 minExtents = Vector2.Zero, maxExtents = Vector2.Zero; + Vector2 visibleMinExtents = Vector2.Zero, visibleMaxExtents = Vector2.Zero; Body farseerBody = null; if (!Hull.hullList.Any(h => h.Submarine == sub)) { @@ -133,8 +135,6 @@ namespace Barotrauma } HullVertices = convexHull; - Vector2 minExtents = Vector2.Zero, maxExtents = Vector2.Zero; - Vector2 visibleMinExtents = Vector2.Zero, visibleMaxExtents = Vector2.Zero; farseerBody = GameMain.World.CreateBody(); farseerBody.UserData = this; @@ -142,25 +142,18 @@ namespace Barotrauma { if (mapEntity.Submarine != submarine || !(mapEntity is Structure wall)) { continue; } + bool hasCollider = wall.HasBody && !wall.IsPlatform && wall.StairDirection == Direction.None; Rectangle rect = wall.Rect; - visibleMinExtents.X = Math.Min(rect.X, visibleMinExtents.X); - visibleMinExtents.Y = Math.Min(rect.Y - rect.Height, visibleMinExtents.Y); - visibleMaxExtents.X = Math.Max(rect.Right, visibleMaxExtents.X); - visibleMaxExtents.Y = Math.Max(rect.Y, visibleMaxExtents.Y); - - if (!wall.HasBody || wall.IsPlatform || wall.StairDirection != Direction.None) { continue; } - - farseerBody.CreateRectangle( - ConvertUnits.ToSimUnits(wall.BodyWidth), - ConvertUnits.ToSimUnits(wall.BodyHeight), - 50.0f, - -wall.BodyRotation, - ConvertUnits.ToSimUnits(new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2) + wall.BodyOffset)).UserData = wall; - - minExtents.X = Math.Min(visibleMinExtents.X, minExtents.X); - minExtents.Y = Math.Min(visibleMinExtents.Y, minExtents.Y); - maxExtents.X = Math.Max(visibleMaxExtents.X, maxExtents.X); - maxExtents.Y = Math.Max(visibleMaxExtents.Y, maxExtents.Y); + SetExtents(new Vector2(rect.X, rect.Y - rect.Height), new Vector2(rect.Right, rect.Y), hasCollider); + if (hasCollider) + { + farseerBody.CreateRectangle( + ConvertUnits.ToSimUnits(wall.BodyWidth), + ConvertUnits.ToSimUnits(wall.BodyHeight), + 50.0f, + -wall.BodyRotation, + ConvertUnits.ToSimUnits(new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2) + wall.BodyOffset)).UserData = wall; + } } foreach (Hull hull in Hull.hullList) @@ -168,21 +161,13 @@ namespace Barotrauma if (hull.Submarine != submarine || hull.IdFreed) { continue; } Rectangle rect = hull.Rect; + SetExtents(new Vector2(rect.X, rect.Y - rect.Height), new Vector2(rect.Right, rect.Y), hasCollider: true); + farseerBody.CreateRectangle( ConvertUnits.ToSimUnits(rect.Width), ConvertUnits.ToSimUnits(rect.Height), 100.0f, ConvertUnits.ToSimUnits(new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2))).UserData = hull; - - visibleMinExtents.X = Math.Min(rect.X, visibleMinExtents.X); - visibleMinExtents.Y = Math.Min(rect.Y - rect.Height, visibleMinExtents.Y); - visibleMaxExtents.X = Math.Max(rect.Right, visibleMaxExtents.X); - visibleMaxExtents.Y = Math.Max(rect.Y, visibleMaxExtents.Y); - - minExtents.X = Math.Min(visibleMinExtents.X, minExtents.X); - minExtents.Y = Math.Min(visibleMinExtents.Y, minExtents.Y); - maxExtents.X = Math.Max(visibleMaxExtents.X, maxExtents.X); - maxExtents.Y = Math.Max(visibleMaxExtents.Y, maxExtents.Y); } foreach (Item item in Item.ItemList) @@ -207,31 +192,21 @@ namespace Barotrauma if (width > 0.0f && height > 0.0f) { item.StaticFixtures.Add(farseerBody.CreateRectangle(simWidth, simHeight, 5.0f, simPos)); - - visibleMinExtents.X = Math.Min(item.Position.X - width / 2, visibleMinExtents.X); - visibleMinExtents.Y = Math.Min(item.Position.Y - height / 2, visibleMinExtents.Y); - visibleMaxExtents.X = Math.Max(item.Position.X + width / 2, visibleMaxExtents.X); - visibleMaxExtents.Y = Math.Max(item.Position.Y + height / 2, visibleMaxExtents.Y); + SetExtents(item.Position - new Vector2(width, height) / 2, item.Position + new Vector2(width, height) / 2, hasCollider: true); } else if (radius > 0.0f && width > 0.0f) { item.StaticFixtures.Add(farseerBody.CreateRectangle(simWidth, simRadius * 2, 5.0f, simPos)); item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitX * simWidth / 2)); item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitX * simWidth / 2)); - visibleMinExtents.X = Math.Min(item.Position.X - width / 2 - radius, visibleMinExtents.X); - visibleMinExtents.Y = Math.Min(item.Position.Y - radius, visibleMinExtents.Y); - visibleMaxExtents.X = Math.Max(item.Position.X + width / 2 + radius, visibleMaxExtents.X); - visibleMaxExtents.Y = Math.Max(item.Position.Y + radius, visibleMaxExtents.Y); + SetExtents(item.Position - new Vector2(width / 2 + radius, height / 2), item.Position + new Vector2(width / 2 + radius, height / 2), hasCollider: true); } else if (radius > 0.0f && height > 0.0f) { item.StaticFixtures.Add(farseerBody.CreateRectangle(simRadius * 2, height, 5.0f, simPos)); item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitY * simHeight / 2)); item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitX * simHeight / 2)); - visibleMinExtents.X = Math.Min(item.Position.X - radius, visibleMinExtents.X); - visibleMinExtents.Y = Math.Min(item.Position.Y - height / 2 - radius, visibleMinExtents.Y); - visibleMaxExtents.X = Math.Max(item.Position.X + radius, visibleMaxExtents.X); - visibleMaxExtents.Y = Math.Max(item.Position.Y + height / 2 + radius, visibleMaxExtents.Y); + SetExtents(item.Position - new Vector2(width / 2, height / 2 + radius), item.Position + new Vector2(width / 2, height / 2 + radius), hasCollider: true); } else if (radius > 0.0f) { @@ -240,12 +215,8 @@ namespace Barotrauma visibleMinExtents.Y = Math.Min(item.Position.Y - radius, visibleMinExtents.Y); visibleMaxExtents.X = Math.Max(item.Position.X + radius, visibleMaxExtents.X); visibleMaxExtents.Y = Math.Max(item.Position.Y + radius, visibleMaxExtents.Y); + SetExtents(item.Position - new Vector2(radius, radius), item.Position + new Vector2(radius, radius), hasCollider: true); } - item.StaticFixtures.ForEach(f => f.UserData = item); - minExtents.X = Math.Min(visibleMinExtents.X, minExtents.X); - minExtents.Y = Math.Min(visibleMinExtents.Y, minExtents.Y); - maxExtents.X = Math.Max(visibleMaxExtents.X, maxExtents.X); - maxExtents.Y = Math.Max(visibleMaxExtents.Y, maxExtents.Y); } Borders = new Rectangle((int)minExtents.X, (int)maxExtents.Y, (int)(maxExtents.X - minExtents.X), (int)(maxExtents.Y - minExtents.Y)); @@ -271,6 +242,21 @@ namespace Barotrauma farseerBody.UserData = submarine; Body = new PhysicsBody(farseerBody); + + void SetExtents(Vector2 min, Vector2 max, bool hasCollider) + { + visibleMinExtents.X = Math.Min(min.X, visibleMinExtents.X); + visibleMinExtents.Y = Math.Min(min.Y, visibleMinExtents.Y); + visibleMaxExtents.X = Math.Max(max.X, visibleMaxExtents.X); + visibleMaxExtents.Y = Math.Max(max.Y, visibleMaxExtents.Y); + if (hasCollider) + { + minExtents.X = Math.Min(min.X, minExtents.X); + minExtents.Y = Math.Min(min.Y, minExtents.Y); + maxExtents.X = Math.Max(max.X, maxExtents.X); + maxExtents.Y = Math.Max(max.Y, maxExtents.Y); + } + } } private List GenerateConvexHull() @@ -853,6 +839,8 @@ namespace Barotrauma Vector2 impulse = direction * impact * 0.5f; impulse = impulse.ClampLength(MaxCollisionImpact); + float impulseMagnitude = impulse.Length(); + if (!MathUtils.IsValid(impulse)) { string errorMsg = @@ -919,8 +907,9 @@ namespace Barotrauma foreach (Item item in Item.ItemList) { if (item.Submarine != submarine || item.CurrentHull == null || item.body == null || !item.body.Enabled) { continue; } + if (item.body.Mass > impulseMagnitude) { continue; } - item.body.ApplyLinearImpulse(item.body.Mass * impulse, 10.0f); + item.body.ApplyLinearImpulse(impulse, 10.0f); item.PositionUpdateInterval = 0.0f; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs index d8dc6665f..20689d456 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -521,7 +521,7 @@ namespace Barotrauma } //saving/loading ---------------------------------------------------- - public bool SaveAs(string filePath, System.IO.MemoryStream previewImage = null) + public void SaveAs(string filePath, System.IO.MemoryStream previewImage = null) { var newElement = new XElement( SubmarineElement.Name, @@ -543,18 +543,9 @@ namespace Barotrauma { doc.Root.Add(new XAttribute("previewimage", Convert.ToBase64String(previewImage.ToArray()))); } - try - { - SaveUtil.CompressStringToFile(filePath, doc.ToString()); - Md5Hash.RemoveFromCache(filePath); - } - catch (Exception e) - { - DebugConsole.ThrowError("Saving submarine \"" + filePath + "\" failed!", e); - return false; - } - return true; + SaveUtil.CompressStringToFile(filePath, doc.ToString()); + Md5Hash.RemoveFromCache(filePath); } public static void AddToSavedSubs(SubmarineInfo subInfo) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs index f6cfdd76e..4f74e2fb5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs @@ -220,25 +220,28 @@ namespace Barotrauma.Networking case ChatMessageType.Order: if (receiver != null && !receiver.IsDead) { - var receiverItem = receiver.Inventory?.AllItems.FirstOrDefault(i => i.GetComponent() != null); - //character doesn't have a radio -> don't send - if (receiverItem == null || !receiver.HasEquippedItem(receiverItem)) { return spokenMsg; } - - var senderItem = sender.Inventory?.AllItems.FirstOrDefault(i => i.GetComponent() != null); - if (senderItem == null || !sender.HasEquippedItem(senderItem)) { return spokenMsg; } - - var receiverRadio = receiverItem.GetComponent(); - var senderRadio = senderItem.GetComponent(); - - if (!receiverRadio.CanReceive(senderRadio)) { return spokenMsg; } - - string msg = ApplyDistanceEffect(receiverItem, senderItem, message, senderRadio.Range); - if (sender.SpeechImpediment > 0.0f) + foreach (Item receiverItem in receiver.Inventory?.AllItems.Where(i => i.GetComponent()?.LinkToChat ?? false)) { - //speech impediment doesn't reduce the range when using a radio, but adds extra garbling - msg = ApplyDistanceEffect(msg, sender.SpeechImpediment / 100.0f); + if (!receiver.HasEquippedItem(receiverItem)) { continue; } + + foreach (Item senderItem in sender.Inventory?.AllItems.Where(i => i.GetComponent()?.LinkToChat ?? false)) + { + if (!sender.HasEquippedItem(senderItem)) { continue; } + + var receiverRadio = receiverItem.GetComponent(); + var senderRadio = senderItem.GetComponent(); + if (!receiverRadio.CanReceive(senderRadio)) { continue; } + + string msg = ApplyDistanceEffect(receiverItem, senderItem, message, senderRadio.Range); + if (sender.SpeechImpediment > 0.0f) + { + //speech impediment doesn't reduce the range when using a radio, but adds extra garbling + msg = ApplyDistanceEffect(msg, sender.SpeechImpediment / 100.0f); + } + return msg; + } } - return msg; + return spokenMsg; } break; } @@ -268,7 +271,7 @@ namespace Barotrauma.Networking foreach (Item item in sender.Inventory.AllItems) { var wifiComponent = item.GetComponent(); - if (wifiComponent == null || !wifiComponent.CanTransmit() || !sender.HasEquippedItem(item)) { continue; } + if (wifiComponent == null || !wifiComponent.LinkToChat || !wifiComponent.CanTransmit() || !sender.HasEquippedItem(item)) { continue; } if (radio == null || wifiComponent.Range > radio.Range) { radio = wifiComponent; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs new file mode 100644 index 000000000..a30e4476c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs @@ -0,0 +1,547 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; + +namespace Barotrauma +{ + /// + /// Marks fields and properties as to be serialized and deserialized by . + /// Also contains settings for some types like maximum and minimum values for numbers to reduce bits used. + /// + /// + /// + /// struct NetPurchasedItem : INetSerializableStruct + /// { + /// [NetworkSerialize] + /// public string Identifier; + /// + /// [NetworkSerialize(ArrayMaxSize = 16)] + /// public string[] Tags; + /// + /// [NetworkSerialize(MinValueInt = 0, MaxValueInt = 8)] + /// public int Amount; + /// } + /// + /// + /// + /// Using the attribute on the struct will make all fields and properties serialized + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Struct | AttributeTargets.Property)] + public class NetworkSerialize : Attribute + { + public int MaxValueInt = int.MaxValue; + public int MinValueInt = int.MinValue; + public float MaxValueFloat = float.MaxValue; + public float MinValueFloat = float.MinValue; + public int NumberOfBits = 8; + public bool IncludeColorAlpha = false; + public int ArrayMaxSize = ushort.MaxValue; + } + + /// + /// Static class that contains serialize and deserialize functions for different types used in + /// + public static class NetSerializableProperties + { + public readonly struct ReadWriteBehavior + { + public delegate dynamic? ReadDelegate(IReadMessage inc, Type type, NetworkSerialize attribute); + public delegate void WriteDelegate(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg); + + public readonly ReadDelegate ReadAction; + public readonly WriteDelegate WriteAction; + + public ReadWriteBehavior(ReadDelegate readAction, WriteDelegate writeAction) + { + ReadAction = readAction; + WriteAction = writeAction; + } + } + + private static readonly ImmutableDictionary TypeBehaviors = new Dictionary + { + { typeof(Boolean), new ReadWriteBehavior(ReadBoolean, WriteDynamic) }, + { typeof(Byte), new ReadWriteBehavior(ReadByte, WriteDynamic) }, + { typeof(UInt16), new ReadWriteBehavior(ReadUInt16, WriteDynamic) }, + { typeof(Int16), new ReadWriteBehavior(ReadInt16, WriteDynamic) }, + { typeof(UInt32), new ReadWriteBehavior(ReadUInt32, WriteDynamic) }, + { typeof(Int32), new ReadWriteBehavior(ReadInt32, WriteInt32) }, + { typeof(UInt64), new ReadWriteBehavior(ReadUInt64, WriteDynamic) }, + { typeof(Int64), new ReadWriteBehavior(ReadInt64, WriteDynamic) }, + { typeof(Single), new ReadWriteBehavior(ReadSingle, WriteSingle) }, + { typeof(Double), new ReadWriteBehavior(ReadDouble, WriteDynamic) }, + { typeof(String), new ReadWriteBehavior(ReadString, WriteDynamic) }, + { typeof(Color), new ReadWriteBehavior(ReadColor, WriteColor) }, + { typeof(Vector2), new ReadWriteBehavior(ReadVector2, WriteVector2) } + }.ToImmutableDictionary(); + + private static readonly ReadWriteBehavior InvalidReadWriteBehavior = new ReadWriteBehavior(ReadInvalid, WriteInvalid); + + private static readonly ImmutableDictionary, ReadWriteBehavior> TypePredicates = new Dictionary, ReadWriteBehavior> + { + // Arrays + { type => type.BaseType?.IsAssignableFrom(typeof(Array)) ?? false, new ReadWriteBehavior(ReadArray, WriteArray) }, + + // Nested INetSerializableStructs + { type => typeof(INetSerializableStruct).IsAssignableFrom(type), new ReadWriteBehavior(ReadINetSerializableStruct, WriteINetSerializableStruct) }, + + // Enums + { type => type.IsEnum, new ReadWriteBehavior(ReadEnum, WriteEnum) }, + + // Nullable / Optional types + { type => Nullable.GetUnderlyingType(type) != null, new ReadWriteBehavior(ReadNullable, WriteNullable) } + }.ToImmutableDictionary(); + + private static void WriteInvalid(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) => throw new InvalidOperationException($"Type {obj?.GetType()} cannot be serialized. Did you forget to implement INetSerializableStruct?"); + + private static dynamic ReadInvalid(IReadMessage inc, Type type, NetworkSerialize attribute) => throw new InvalidOperationException($"Type {type} cannot be deserialized. Did you forget to implement INetSerializableStruct?"); + + private static void WriteNullable(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) + { + if (obj is { } notNull) + { + msg.Write(true); + + if (TryFindBehavior(notNull.GetType(), out ReadWriteBehavior behavior)) + { + // uh oh, something terrible has happened! + if (behavior.WriteAction == WriteNullable) { behavior = InvalidReadWriteBehavior; } + + behavior.WriteAction(notNull, attribute, msg); + return; + } + } + + msg.Write(false); + } + + private static dynamic? ReadNullable(IReadMessage inc, Type type, NetworkSerialize attribute) + { + if (!inc.ReadBoolean()) { return null; } + + Type? underlyingType = Nullable.GetUnderlyingType(type); + if (underlyingType is null) { throw new InvalidOperationException($"Could not get the underlying type of {type} in {nameof(ReadNullable)}"); } + + if (TryFindBehavior(underlyingType, out ReadWriteBehavior behavior)) + { + // uh oh, something terrible has happened! + if (behavior.ReadAction == ReadNullable) { behavior = InvalidReadWriteBehavior; } + + return behavior.ReadAction(inc, underlyingType, attribute); + } + + throw new InvalidOperationException($"Could not find suitable behavior for type {underlyingType} in {nameof(ReadNullable)}"); + } + + private static void WriteEnum(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) + { + if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); } + + Range range = GetEnumRange(obj.GetType()); + msg.WriteRangedInteger(Convert.ChangeType(obj, obj.GetTypeCode()), range.Start, range.End); + } + + private static dynamic ReadEnum(IReadMessage inc, Type type, NetworkSerialize attribute) + { + Range range = GetEnumRange(type); + int enumIndex = inc.ReadRangedInteger(range.Start, range.End); + + foreach (dynamic e in Enum.GetValues(type)) + { + if (Convert.ChangeType(e, e.GetTypeCode()) == enumIndex) { return e; } + } + + throw new InvalidOperationException($"An enum {type} with value {enumIndex} could not be found in {nameof(ReadEnum)}"); + } + + private static void WriteINetSerializableStruct(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) + { + if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); } + + if (!(obj is INetSerializableStruct serializableStruct)) { throw new InvalidOperationException($"Object in {nameof(WriteINetSerializableStruct)} was {obj.GetType()} but expected {nameof(INetSerializableStruct)}"); } + + serializableStruct.Write(msg); + } + + private static dynamic ReadINetSerializableStruct(IReadMessage inc, Type type, NetworkSerialize attribute) + { + return INetSerializableStruct.ReadDynamic(type, inc); + } + + private static void WriteDynamic(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) + { + if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); } + + msg.Write(obj); + } + + private static dynamic ReadArray(IReadMessage inc, Type type, NetworkSerialize attribute) + { + Type? elementType = type.GetElementType(); + if (elementType is null) { throw new InvalidOperationException($"Could not get the element type of {type} in {nameof(ReadArray)}"); } + + int length = inc.ReadRangedInteger(0, attribute.ArrayMaxSize); + + Array list = Array.CreateInstance(elementType, length); + + for (int i = 0; i < length; i++) + { + if (TryFindBehavior(elementType, out ReadWriteBehavior behavior)) + { + list.SetValue(behavior.ReadAction(inc, elementType, attribute), i); + } + else + { + throw new InvalidOperationException($"Could not find suitable behavior for type {elementType} in {nameof(ReadArray)}"); + } + } + + return list; + } + + private static void WriteArray(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) + { + if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); } + + if (!(obj is Array array)) { throw new InvalidOperationException($"Object in {nameof(WriteArray)} was {obj.GetType()} but expected {nameof(Array)}"); } + + msg.WriteRangedInteger(array.Length, 0, attribute.ArrayMaxSize); + + foreach (dynamic o in array) + { + if (TryFindBehavior(o.GetType(), out ReadWriteBehavior behavior)) + { + behavior.WriteAction(o, attribute, msg); + } + } + } + + private static dynamic ReadBoolean(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadBoolean(); + + private static dynamic ReadByte(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadByte(); + + private static dynamic ReadUInt16(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadUInt16(); + + private static dynamic ReadInt16(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadInt16(); + + private static dynamic ReadUInt32(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadUInt32(); + + private static dynamic ReadInt32(IReadMessage inc, Type type, NetworkSerialize attribute) + { + if (IsRanged(attribute.MinValueInt, attribute.MaxValueInt)) + { + return inc.ReadRangedInteger(attribute.MinValueInt, attribute.MaxValueInt); + } + + return inc.ReadInt32(); + } + + private static void WriteInt32(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) + { + if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); } + + if (IsRanged(attribute.MinValueInt, attribute.MaxValueInt)) + { + msg.WriteRangedInteger(obj, attribute.MinValueInt, attribute.MaxValueInt); + return; + } + + msg.Write(obj); + } + + private static dynamic ReadUInt64(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadUInt64(); + + private static dynamic ReadInt64(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadInt64(); + + private static dynamic ReadSingle(IReadMessage inc, Type type, NetworkSerialize attribute) + { + if (IsRanged(attribute.MinValueFloat, attribute.MaxValueFloat)) + { + return inc.ReadRangedSingle(attribute.MinValueFloat, attribute.MaxValueFloat, attribute.NumberOfBits); + } + + return inc.ReadSingle(); + } + + private static void WriteSingle(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) + { + if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); } + + if (IsRanged(attribute.MinValueFloat, attribute.MaxValueFloat)) + { + msg.WriteRangedSingle(obj, attribute.MinValueFloat, attribute.MaxValueFloat, attribute.NumberOfBits); + return; + } + + msg.Write(obj); + } + + private static dynamic ReadDouble(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadDouble(); + + private static dynamic ReadString(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadString(); + + private static dynamic ReadColor(IReadMessage inc, Type type, NetworkSerialize attribute) => attribute.IncludeColorAlpha ? inc.ReadColorR8G8B8A8() : inc.ReadColorR8G8B8(); + + private static void WriteColor(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) + { + if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); } + + if (attribute.IncludeColorAlpha) + { + msg.WriteColorR8G8B8A8(obj); + return; + } + + msg.WriteColorR8G8B8(obj); + } + + private static dynamic ReadVector2(IReadMessage inc, Type type, NetworkSerialize attribute) + { + float x; + float y; + + if (IsRanged(attribute.MinValueFloat, attribute.MaxValueFloat)) + { + x = inc.ReadRangedSingle(attribute.MinValueFloat, attribute.MaxValueFloat, attribute.NumberOfBits); + y = inc.ReadRangedSingle(attribute.MinValueFloat, attribute.MaxValueFloat, attribute.NumberOfBits); + } + else + { + x = inc.ReadSingle(); + y = inc.ReadSingle(); + } + + return new Vector2(x, y); + } + + private static void WriteVector2(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) + { + if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); } + + var (x, y) = (Vector2)obj; + if (IsRanged(attribute.MinValueFloat, attribute.MaxValueFloat)) + { + msg.WriteRangedSingle(x, attribute.MinValueFloat, attribute.MaxValueFloat, attribute.NumberOfBits); + msg.WriteRangedSingle(y, attribute.MinValueFloat, attribute.MaxValueFloat, attribute.NumberOfBits); + return; + } + + msg.Write(x); + msg.Write(y); + } + + private static bool IsRanged(float minValue, float maxValue) => minValue > float.MinValue || maxValue < float.MaxValue; + private static bool IsRanged(int minValue, int maxValue) => minValue > int.MinValue || maxValue < int.MaxValue; + + private static Range GetEnumRange(Type type) + { + ImmutableArray values = Enum.GetValues(type).Cast().ToImmutableArray(); + return new Range(values.Min(), values.Max()); + } + + public static bool TryFindBehavior(Type type, out ReadWriteBehavior behavior) + { + if (TypeBehaviors.TryGetValue(type, out behavior)) { return true; } + + foreach (var (predicate, behavior2) in TypePredicates) + { + if (predicate(type)) + { + behavior = behavior2; + return true; + } + } + + behavior = InvalidReadWriteBehavior; + return false; + } + } + + /// + /// Interface that allows the creation of automatically serializable and deserializable structs. + ///

+ ///
+ /// + /// + /// public enum PurchaseResult + /// { + /// Unknown, + /// Completed, + /// Declined + /// } + /// + /// [NetworkSerialize] + /// struct NetStoreTransaction : INetSerializableStruct + /// { + /// public long Timestamp { get; set; } + /// public PurchaseResult Result { get; set; } + /// public NetPurchasedItem? PurchasedItem { get; set; } + /// } + /// + /// [NetworkSerialize] + /// struct NetPurchasedItem : INetSerializableStruct + /// { + /// public string Identifier; + /// public string[] Tags; + /// public int Amount; + /// } + /// + /// + /// + /// Supported types are:
+ /// bool
+ /// byte
+ /// ushort
+ /// short
+ /// uint
+ /// int
+ /// ulong
+ /// long
+ /// float
+ /// double
+ /// string
+ ///
+ ///
+ /// In addition arrays, enums and are supported.
+ /// Using will make the field or property optional + ///
+ /// + public interface INetSerializableStruct + { + /// + /// Deserializes a network message into a struct. + /// + /// + /// + /// public void ClientRead(IReadMessage inc) + /// { + /// NetStoreTransaction transaction = INetSerializableStruct.Read<NetStoreTransaction>(inc); + /// if (transaction.Result == PurchaseResult.Declined) + /// { + /// Console.WriteLine("Purchase declined!"); + /// return; + /// } + /// + /// if (transaction.PurchasedItem is { } item) + /// { + /// // Purchased 3x Wrench with tags: smallitem, mechanical, tool + /// Console.WriteLine($"Purchased {item.Amount}x {item.Identifier} with tags: {string.Join(", ", item.Tags)}"); + /// } + /// } + /// + /// + /// Incoming network message + /// Type of the struct that implements + /// A new struct of type T with fields and properties deserialized + public static T Read(IReadMessage inc) where T : INetSerializableStruct => (T)ReadDynamic(typeof(T), inc); + + public static dynamic ReadDynamic(Type type, IReadMessage inc) + { + object? newObject = Activator.CreateInstance(type); + if (newObject is null) { return default!; } + + PropertyInfo[] properties = type.GetProperties(); + foreach (PropertyInfo info in properties) + { + NetworkSerialize? attribute = GetAttribute(info, newObject); + if (attribute is null) { continue; } + + if (NetSerializableProperties.TryFindBehavior(info.PropertyType, out var behavior)) + { + object? value = behavior.ReadAction(inc, info.PropertyType, attribute); + info.SetValue(newObject, value); + } + else + { + DebugConsole.ThrowError($"Unsupported property type \"{info.PropertyType}\" in {newObject}!"); + } + } + + FieldInfo[] fields = type.GetFields(); + foreach (FieldInfo info in fields) + { + NetworkSerialize? attribute = GetAttribute(info, newObject); + if (attribute is null) { continue; } + + if (NetSerializableProperties.TryFindBehavior(info.FieldType, out var behavior)) + { + object? value = behavior.ReadAction(inc, info.FieldType, attribute); + info.SetValue(newObject, value); + } + else + { + DebugConsole.ThrowError($"Unsupported field type \"{info.FieldType}\" in {newObject}!"); + } + } + + return newObject; + } + + /// + /// Serializes the struct into a network message + /// + /// + /// public void ServerWrite(IWriteMessage msg) + /// { + /// INetSerializableStruct transaction = new NetStoreTransaction + /// { + /// Result = PurchaseResult.Completed, + /// Timestamp = DateTimeOffset.Now.ToUnixTimeSeconds(), + /// PurchasedItem = new NetPurchasedItem + /// { + /// Identifier = "Wrench", + /// Amount = 3, + /// Tags = new []{ "smallitem", "mechanical", "tool" } + /// } + /// }; + /// + /// transaction.Write(msg); + /// } + /// + /// + /// + /// Outgoing network message + public void Write(IWriteMessage msg) + { + PropertyInfo[] properties = GetType().GetProperties(); + foreach (PropertyInfo info in properties) + { + NetworkSerialize? attribute = GetAttribute(info, this); + if (attribute is null) { continue; } + + if (NetSerializableProperties.TryFindBehavior(info.PropertyType, out var behavior)) + { + behavior.WriteAction(info.GetValue(this), attribute, msg); + } + else + { + throw new InvalidOperationException($"Unsupported property type \"{info.PropertyType}\" in {this}"); + } + } + + FieldInfo[] fields = GetType().GetFields(); + foreach (FieldInfo info in fields) + { + NetworkSerialize? attribute = GetAttribute(info, this); + if (attribute is null) { continue; } + + if (NetSerializableProperties.TryFindBehavior(info.FieldType, out var behavior)) + { + behavior.WriteAction(info.GetValue(this), attribute, msg); + } + else + { + throw new InvalidOperationException($"Unsupported field type \"{info.FieldType}\" in {this}"); + } + } + } + + private static NetworkSerialize? GetAttribute(MemberInfo info, object baseClass) => info.GetCustomAttribute() ?? baseClass.GetType().GetCustomAttribute(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index 4894b2a32..032971460 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -29,7 +29,8 @@ namespace Barotrauma.Networking REQUEST_STARTGAMEFINALIZE, //tell the server you're ready to finalize round initialization ERROR, //tell the server that an error occurred - CREW, + CREW, //hiring UI + MEDICAL, //medical clinic READY_CHECK, READY_TO_SPAWN @@ -80,7 +81,8 @@ namespace Barotrauma.Networking MISSION, EVENTACTION, CREW, //anything related to managing bots in multiplayer - READY_CHECK //start, end and update a ready check + MEDICAL, //medical clinic + READY_CHECK //start, end and update a ready check } enum ServerNetObject { @@ -144,7 +146,9 @@ namespace Barotrauma.Networking NotOnWhitelist, ExcessiveDesyncOldEvent, ExcessiveDesyncRemovedEvent, - SyncTimeout + SyncTimeout, + SteamP2PError, + SteamP2PTimeOut, } abstract partial class NetworkMember diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs index dc79afcd9..81170bc0c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs @@ -23,18 +23,22 @@ namespace Barotrauma.Networking ///
public int? WallSectionIndex { get; set; } + public bool IsNewOrder { get; } + /// - /// Same as calling , but the text parameter is set using + /// Same as calling , + /// but the text parameter is set using /// - public OrderChatMessage(Order order, string orderOption, int priority, ISpatialEntity targetEntity, Character targetCharacter, Character sender) + public OrderChatMessage(Order order, string orderOption, int priority, ISpatialEntity targetEntity, Character targetCharacter, Character sender, bool isNewOrder = true) : this(order, orderOption, priority, - order?.GetChatMessage(targetCharacter?.Name, sender?.CurrentHull?.DisplayName, givingOrderToSelf: targetCharacter == sender, orderOption: orderOption, priority: priority), - targetEntity, targetCharacter, sender) + order?.GetChatMessage(targetCharacter?.Name, sender?.CurrentHull?.DisplayName, targetCharacter == sender, orderOption, isNewOrder), + targetEntity, targetCharacter, sender, isNewOrder) { } - public OrderChatMessage(Order order, string orderOption, int priority, string text, ISpatialEntity targetEntity, Character targetCharacter, Character sender) + public OrderChatMessage(Order order, string orderOption, int priority, string text, ISpatialEntity targetEntity, + Character targetCharacter, Character sender, bool isNewOrder = true) : base(sender?.Name, text, ChatMessageType.Order, sender, GameMain.NetworkMember.ConnectedClients.Find(c => c.Character == sender)) { Order = order; @@ -42,9 +46,11 @@ namespace Barotrauma.Networking OrderPriority = priority; TargetCharacter = targetCharacter; TargetEntity = targetEntity; + IsNewOrder = isNewOrder; } - public static void WriteOrder(IWriteMessage msg, Order order, Character targetCharacter, ISpatialEntity targetEntity, string orderOption, int orderPriority, int? wallSectionIndex) + public static void WriteOrder(IWriteMessage msg, Order order, Character targetCharacter, ISpatialEntity targetEntity, + string orderOption, int orderPriority, int? wallSectionIndex, bool isNewOrder) { msg.Write((byte)Order.PrefabList.IndexOf(order.Prefab)); msg.Write(targetCharacter == null ? (UInt16)0 : targetCharacter.ID); @@ -100,11 +106,13 @@ namespace Barotrauma.Networking msg.Write((byte)(wallSectionIndex ?? order.WallSectionIndex ?? 0)); } } + + msg.Write(isNewOrder); } private void WriteOrder(IWriteMessage msg) { - WriteOrder(msg, Order, TargetCharacter, TargetEntity, OrderOption, OrderPriority, WallSectionIndex); + WriteOrder(msg, Order, TargetCharacter, TargetEntity, OrderOption, OrderPriority, WallSectionIndex, IsNewOrder); } public struct OrderMessageInfo @@ -119,8 +127,10 @@ namespace Barotrauma.Networking public OrderTarget TargetPosition { get; } public int? WallSectionIndex { get; } public int Priority { get; } + public bool IsNewOrder { get; } - public OrderMessageInfo(int orderIndex, Order orderPrefab, string orderOption, int? orderOptionIndex, Character targetCharacter, Order.OrderTargetType targetType, Entity targetEntity, OrderTarget targetPosition, int? wallSectionIndex, int orderPriority) + public OrderMessageInfo(int orderIndex, Order orderPrefab, string orderOption, int? orderOptionIndex, Character targetCharacter, + Order.OrderTargetType targetType, Entity targetEntity, OrderTarget targetPosition, int? wallSectionIndex, int orderPriority, bool isNewOrder) { OrderIndex = orderIndex; OrderPrefab = orderPrefab; @@ -132,6 +142,7 @@ namespace Barotrauma.Networking TargetPosition = targetPosition; WallSectionIndex = wallSectionIndex; Priority = orderPriority; + IsNewOrder = isNewOrder; } } @@ -205,7 +216,10 @@ namespace Barotrauma.Networking wallSectionIndex = msg.ReadByte(); } - return new OrderMessageInfo(orderIndex, orderPrefab, orderOption, optionIndex, targetCharacter, orderTargetType, targetEntity, orderTargetPosition, wallSectionIndex, orderPriority); + bool isNewOrder = msg.ReadBoolean(); + + return new OrderMessageInfo(orderIndex, orderPrefab, orderOption, optionIndex, targetCharacter, + orderTargetType, targetEntity, orderTargetPosition, wallSectionIndex, orderPriority, isNewOrder); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Enums.cs index e3ccaab6c..8f8e62d55 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Enums.cs @@ -38,19 +38,19 @@ namespace Barotrauma.Networking public static class NetworkEnumExtensions { public static bool IsCompressed(this PacketHeader h) - => h.IsBitSet(PacketHeader.IsCompressed); + => h.HasFlag(PacketHeader.IsCompressed); public static bool IsConnectionInitializationStep(this PacketHeader h) - => h.IsBitSet(PacketHeader.IsConnectionInitializationStep); + => h.HasFlag(PacketHeader.IsConnectionInitializationStep); public static bool IsDisconnectMessage(this PacketHeader h) - => h.IsBitSet(PacketHeader.IsDisconnectMessage); + => h.HasFlag(PacketHeader.IsDisconnectMessage); public static bool IsServerMessage(this PacketHeader h) - => h.IsBitSet(PacketHeader.IsServerMessage); + => h.HasFlag(PacketHeader.IsServerMessage); public static bool IsHeartbeatMessage(this PacketHeader h) - => h.IsBitSet(PacketHeader.IsHeartbeatMessage); + => h.HasFlag(PacketHeader.IsHeartbeatMessage); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs index 8d27d37ac..8367fdce9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs @@ -81,8 +81,7 @@ namespace Barotrauma.Networking public const string SavePath = "ServerLogs"; private readonly Queue lines; - - private int unsavedLineCount; + private readonly Queue unsavedLines; private readonly bool[] msgTypeHidden = new bool[Enum.GetValues(typeof(MessageType)).Length]; @@ -98,6 +97,7 @@ namespace Barotrauma.Networking { ServerName = serverName; lines = new Queue(); + unsavedLines = new Queue(); foreach (MessageType messageType in Enum.GetValues(typeof(MessageType))) { @@ -117,22 +117,19 @@ namespace Barotrauma.Networking #endif lines.Enqueue(newText); + unsavedLines.Enqueue(newText); #if CLIENT if (listBox != null) { AddLine(newText); - listBox.UpdateScrollBarSize(); } #endif - - unsavedLineCount++; - - if (unsavedLineCount >= LinesPerFile) + if (unsavedLines.Count() >= LinesPerFile) { Save(); - unsavedLineCount = 0; + unsavedLines.Clear(); } while (lines.Count > LinesPerFile) @@ -176,7 +173,7 @@ namespace Barotrauma.Networking try { - File.WriteAllLines(filePath, lines.Select(l => l.SanitizedText)); + File.WriteAllLines(filePath, unsavedLines.Select(l => l.SanitizedText)); } catch (Exception e) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index 637ddd16a..d4031ebc6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -97,7 +97,7 @@ namespace Barotrauma.Networking private readonly SerializableProperty property; private readonly string typeString; private readonly object parentObject; - + public string Name { get { return property.Name; } @@ -214,7 +214,7 @@ namespace Barotrauma.Networking public void Write(IWriteMessage msg, object overrideValue = null) { - if (overrideValue == null) overrideValue = property.GetValue(parentObject); + if (overrideValue == null) { overrideValue = Value; } switch (typeString) { case "float": @@ -314,6 +314,7 @@ namespace Barotrauma.Networking { NetPropertyData netPropertyData = new NetPropertyData(this, property, typeName); UInt32 key = ToolBox.StringToUInt32Hash(property.Name, md5); + if (key == 0) { key++; } //0 is reserved to indicate the end of the netproperties section of a message if (netProperties.ContainsKey(key)){ throw new Exception("Hashing collision in ServerSettings.netProperties: " + netProperties[key] + " has same key as " + property.Name + " (" + key.ToString() + ")"); } netProperties.Add(key, netPropertyData); } @@ -717,13 +718,6 @@ namespace Barotrauma.Networking set; } - [Serialize("", true)] - public string CampaignSubmarines - { - get; - set; - } - private YesNoMaybe traitorsEnabled; [Serialize(YesNoMaybe.No, true)] public YesNoMaybe TraitorsEnabled @@ -1067,7 +1061,6 @@ namespace Barotrauma.Networking } #if SERVER - MultiPlayerCampaign.UpdateCampaignSubs(); SelectNonHiddenSubmarine(); #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs index 98e6022d1..f4a80591c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs @@ -136,6 +136,10 @@ namespace Barotrauma.Steam DebugConsole.NewMessage("Failed to increment stat \"" + statName + "\"."); #endif } + else + { + StoreStats(); + } return success; } @@ -150,6 +154,10 @@ namespace Barotrauma.Steam DebugConsole.NewMessage("Failed to increment stat \"" + statName + "\"."); #endif } + else + { + StoreStats(); + } return success; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index 5b4fd2ac9..6a21d2d52 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -88,6 +88,7 @@ namespace Barotrauma Circle, Rectangle, Capsule, HorizontalCapsule }; + public const float MinDensity = 0.01f; public const float DefaultAngularDamping = 5.0f; private static readonly List list = new List(); @@ -330,6 +331,7 @@ namespace Barotrauma public PhysicsBody(float width, float height, float radius, float density) { + density = Math.Max(density, MinDensity); CreateBody(width, height, radius, density); LastSentPosition = FarseerBody.Position; list.Add(this); @@ -367,7 +369,7 @@ namespace Barotrauma float radius = ConvertUnits.ToSimUnits(limbParams.Radius) * limbParams.Scale * limbParams.Ragdoll.LimbScale; float height = ConvertUnits.ToSimUnits(limbParams.Height) * limbParams.Scale * limbParams.Ragdoll.LimbScale; float width = ConvertUnits.ToSimUnits(limbParams.Width) * limbParams.Scale * limbParams.Ragdoll.LimbScale; - density = limbParams.Density; + density = Math.Max(limbParams.Density, MinDensity); CreateBody(width, height, radius, density); FarseerBody.BodyType = BodyType.Dynamic; FarseerBody.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel; @@ -386,13 +388,13 @@ namespace Barotrauma 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); + density = Math.Max(element.GetAttributeFloat("density", 10.0f), MinDensity); CreateBody(width, height, radius, density); 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); + FarseerBody.Friction = element.GetAttributeFloat("friction", 0.5f); FarseerBody.Restitution = element.GetAttributeFloat("restitution", 0.05f); FarseerBody.UserData = this; SetTransformIgnoreContacts(position, 0.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index 1f4ff6489..25cedf801 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs @@ -228,13 +228,13 @@ namespace Barotrauma { case "bool": bool boolValue = value == "true" || value == "True"; - if (TrySetValueWithoutReflection(parentObject, boolValue)) { return true; } + if (TrySetBoolValueWithoutReflection(parentObject, boolValue)) { return true; } PropertyInfo.SetValue(parentObject, boolValue, null); break; case "int": if (int.TryParse(value, out int intVal)) { - if (TrySetValueWithoutReflection(parentObject, intVal)) { return true; } + if (TrySetFloatValueWithoutReflection(parentObject, intVal)) { return true; } PropertyInfo.SetValue(parentObject, intVal, null); } else @@ -245,7 +245,7 @@ namespace Barotrauma case "float": if (float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out float floatVal)) { - if (TrySetValueWithoutReflection(parentObject, floatVal)) { return true; } + if (TrySetFloatValueWithoutReflection(parentObject, floatVal)) { return true; } PropertyInfo.SetValue(parentObject, floatVal, null); } else @@ -387,7 +387,7 @@ namespace Barotrauma { try { - if (TrySetValueWithoutReflection(parentObject, value)) { return true; } + if (TrySetFloatValueWithoutReflection(parentObject, value)) { return true; } PropertyInfo.SetValue(parentObject, value, null); } catch (TargetInvocationException e) @@ -408,7 +408,7 @@ namespace Barotrauma { try { - if (TrySetValueWithoutReflection(parentObject, value)) { return true; } + if (TrySetBoolValueWithoutReflection(parentObject, value)) { return true; } PropertyInfo.SetValue(parentObject, value, null); } catch (TargetInvocationException e) @@ -428,6 +428,7 @@ namespace Barotrauma { try { + if (TrySetFloatValueWithoutReflection(parentObject, value)) { return true; } PropertyInfo.SetValue(parentObject, value, null); } catch (TargetInvocationException e) @@ -466,6 +467,56 @@ namespace Barotrauma } } + public float GetFloatValue(object parentObject) + { + if (parentObject == null || PropertyInfo == null) { return 0.0f; } + + if (TryGetFloatValueWithoutReflection(parentObject, out float value)) + { + return value; + } + + try + { + return (float)PropertyInfo.GetValue(parentObject, null); + } + catch (TargetInvocationException e) + { + DebugConsole.ThrowError("Exception thrown by the target of SerializableProperty.GetValue", e.InnerException); + return 0.0f; + } + catch (Exception e) + { + DebugConsole.ThrowError("Error in SerializableProperty.GetValue", e); + return 0.0f; + } + } + + public bool GetBoolValue(object parentObject) + { + if (parentObject == null || PropertyInfo == null) { return false; } + + if (TryGetBoolValueWithoutReflection(parentObject, out bool value)) + { + return value; + } + + try + { + return (bool)PropertyInfo.GetValue(parentObject, null); + } + catch (TargetInvocationException e) + { + DebugConsole.ThrowError("Exception thrown by the target of SerializableProperty.GetValue", e.InnerException); + return false; + } + catch (Exception e) + { + DebugConsole.ThrowError("Error in SerializableProperty.GetValue", e); + return false; + } + } + public static string GetSupportedTypeName(Type type) { if (type.IsEnum) return "Enum"; @@ -481,125 +532,209 @@ namespace Barotrauma ///
private object TryGetValueWithoutReflection(object parentObject) { + if (PropertyType == typeof(float)) + { + if (TryGetFloatValueWithoutReflection(parentObject, out float value)) { return value; } + } + else if (PropertyType == typeof(bool)) + { + if (TryGetBoolValueWithoutReflection(parentObject, out bool value)) { return value; } + } + else if (PropertyType == typeof(string)) + { + if (TryGetStringValueWithoutReflection(parentObject, out string value)) { return value; } + } + return null; + } + + /// + /// Try getting the values of some commonly used properties directly without reflection + /// + private bool TryGetFloatValueWithoutReflection(object parentObject, out float value) + { + value = 0.0f; switch (Name) { - case "Voltage": - if (parentObject is Powered powered) { return powered.Voltage; } + case nameof(Powered.Voltage): + if (parentObject is Powered powered) { value = powered.Voltage; return true; } break; - case "Charge": - if (parentObject is PowerContainer powerContainer) { return powerContainer.Charge; } - break; - case "Overload": - if (parentObject is PowerTransfer powerTransfer) { return powerTransfer.Overload; } - break; - case "AvailableFuel": - { if (parentObject is Reactor reactor) { return reactor.AvailableFuel; } } - break; - case "FissionRate": - { if (parentObject is Reactor reactor) { return reactor.FissionRate; } } - break; - case "OxygenFlow": - if (parentObject is Vent vent) { return vent.OxygenFlow; } - break; - case "CurrFlow": - if (parentObject is Pump pump) { return pump.CurrFlow; } - if (parentObject is OxygenGenerator oxygenGenerator) { return oxygenGenerator.CurrFlow; } - break; - case "CurrentVolume": - if (parentObject is Engine engine) { return engine.CurrentVolume; } - break; - case "MotionDetected": - if (parentObject is MotionSensor motionSensor) { return motionSensor.MotionDetected; } - break; - case "Oxygen": - { if (parentObject is Character character) { return character.Oxygen; } } - break; - case "Health": - { if (parentObject is Character character) { return character.Health; } } - break; - case "OxygenAvailable": - { if (parentObject is Character character) { return character.OxygenAvailable; } } - break; - case "PressureProtection": - { if (parentObject is Character character) { return character.PressureProtection; } } - break; - case "IsDead": - { if (parentObject is Character character) { return character.IsDead; } } - break; - case "IsHuman": - { if (parentObject is Character character) { return character.IsHuman; } } - break; - case "IsOn": - { if (parentObject is LightComponent lightComponent) { return lightComponent.IsOn; } } - break; - case "Condition": + case nameof(PowerContainer.Charge): { - if (parentObject is Item item) { return item.Condition; } + if (parentObject is PowerContainer powerContainer) { value = powerContainer.Charge; return true; } } break; - case "ContainerIdentifier": + case nameof(PowerContainer.ChargePercentage): { - if (parentObject is Item item) { return item.ContainerIdentifier; } + if (parentObject is PowerContainer powerContainer) { value = powerContainer.ChargePercentage; return true; } } break; - case "PhysicsBodyActive": + case nameof(Reactor.AvailableFuel): + { if (parentObject is Reactor reactor) { value = reactor.AvailableFuel; return true; } } + break; + case nameof(Reactor.FissionRate): + { if (parentObject is Reactor reactor) { value = reactor.FissionRate; return true; } } + break; + case nameof(Reactor.Temperature): + { if (parentObject is Reactor reactor) { value = reactor.Temperature; return true; } } + break; + case nameof(Vent.OxygenFlow): + if (parentObject is Vent vent) { value = vent.OxygenFlow; return true; } + break; + case nameof(Pump.CurrFlow): + { if (parentObject is Pump pump) { value = pump.CurrFlow; return true; } } + if (parentObject is OxygenGenerator oxygenGenerator) { value = oxygenGenerator.CurrFlow; return true; } + break; + case nameof(Engine.CurrentBrokenVolume): + { if (parentObject is Engine engine) { value = engine.CurrentBrokenVolume; return true; } } + { if (parentObject is Pump pump) { value = pump.CurrentBrokenVolume; return true; } } + break; + case nameof(Engine.CurrentVolume): + { if (parentObject is Engine engine) { value = engine.CurrentVolume; return true; } } + break; + case nameof(Character.Oxygen): + { if (parentObject is Character character) { value = character.Oxygen; return true; } } + { if (parentObject is Hull hull) { value = hull.Oxygen; return true; } } + break; + case nameof(Character.Health): + { if (parentObject is Character character) { value = character.Health; return true; } } + break; + case nameof(Character.OxygenAvailable): + { if (parentObject is Character character) { value = character.OxygenAvailable; return true; } } + break; + case nameof(Character.PressureProtection): + { if (parentObject is Character character) { value = character.PressureProtection; return true; } } + break; + case nameof(Item.Condition): + { if (parentObject is Item item) { value = item.Condition; return true; } } + break; + case nameof(Character.SpeedMultiplier): + { if (parentObject is Character character) { value = character.SpeedMultiplier; return true; } } + break; + } + return false; + } + + /// + /// Try getting the values of some commonly used properties directly without reflection + /// + private bool TryGetBoolValueWithoutReflection(object parentObject, out bool value) + { + value = false; + switch (Name) + { + case nameof(ItemComponent.IsActive): + if (parentObject is ItemComponent ic) { value = ic.IsActive; return true; } + break; + case nameof(PowerTransfer.Overload): + if (parentObject is PowerTransfer powerTransfer) { value = powerTransfer.Overload; return true; } + break; + case nameof(MotionSensor.MotionDetected): + if (parentObject is MotionSensor motionSensor) { value = motionSensor.MotionDetected; return true; } + break; + case nameof(Character.IsDead): + { if (parentObject is Character character) { value = character.IsDead; return true; } } + break; + case nameof(Character.IsHuman): + { if (parentObject is Character character) { value = character.IsHuman; return true; } } + break; + case nameof(LightComponent.IsOn): + { if (parentObject is LightComponent lightComponent) { value = lightComponent.IsOn; return true; } } + break; + case nameof(Item.PhysicsBodyActive): { - if (parentObject is Item item) { return item.PhysicsBodyActive; } + if (parentObject is Item item) { value = item.PhysicsBodyActive; return true; } + } + break; + case nameof(DockingPort.Docked): + if (parentObject is DockingPort dockingPort) { value = dockingPort.Docked; return true; } + break; + case nameof(Reactor.TemperatureCritical): + if (parentObject is Reactor reactor) { value = reactor.TemperatureCritical; return true; } + break; + case nameof(TriggerComponent.TriggerActive): + if (parentObject is TriggerComponent trigger) { value = trigger.TriggerActive; return true; } + break; + case nameof(Controller.State): + if (parentObject is Controller controller) { value = controller.State; return true; } + break; + } + return false; + } + + /// + /// Try getting the values of some commonly used properties directly without reflection + /// + private bool TryGetStringValueWithoutReflection(object parentObject, out string value) + { + value = null; + switch (Name) + { + case nameof(Item.ContainerIdentifier): + { + if (parentObject is Item item) { value = item.ContainerIdentifier; return true; } } break; } - - return null; + return false; } /// /// Try setting the values of some commonly used properties directly without reflection /// - private bool TrySetValueWithoutReflection(object parentObject, object value) + private bool TrySetFloatValueWithoutReflection(object parentObject, float value) { switch (Name) { - case "Condition": - if (parentObject is Item item && value is float) { item.Condition = (float)value; return true; } + case nameof(Item.Condition): + if (parentObject is Item item) { item.Condition = value; return true; } break; - case "Voltage": - if (parentObject is Powered powered && value is float) { powered.Voltage = (float)value; return true; } + case nameof(Powered.Voltage): + if (parentObject is Powered powered) { powered.Voltage = value; return true; } break; - case "Charge": - if (parentObject is PowerContainer powerContainer && value is float) { powerContainer.Charge = (float)value; return true; } + case nameof(PowerContainer.Charge): + if (parentObject is PowerContainer powerContainer) { powerContainer.Charge = value; return true; } break; - case "AvailableFuel": - if (parentObject is Reactor reactor && value is float) { reactor.AvailableFuel = (float)value; return true; } + case nameof(Reactor.AvailableFuel): + if (parentObject is Reactor reactor) { reactor.AvailableFuel = value; return true; } break; - case "Oxygen": - { if (parentObject is Character character && value is float) { character.Oxygen = (float)value; return true; } } + case nameof(Character.Oxygen): + { if (parentObject is Character character) { character.Oxygen = value; return true; } } break; - case "HideFace": - { if (parentObject is Character character && value is bool) { character.HideFace = (bool)value; return true; } } + case nameof(Character.OxygenAvailable): + { if (parentObject is Character character) { character.OxygenAvailable = value; return true; } } break; - case "OxygenAvailable": - { if (parentObject is Character character && value is float) { character.OxygenAvailable = (float)value; return true; } } + case nameof(Character.PressureProtection): + { if (parentObject is Character character) { character.PressureProtection = value; return true; } } break; - case "ObstructVision": - { if (parentObject is Character character && value is bool) { character.ObstructVision = (bool)value; return true; } } + case nameof(Character.LowPassMultiplier): + { if (parentObject is Character character) { character.LowPassMultiplier = value; return true; } } break; - case "PressureProtection": - { if (parentObject is Character character && value is float) { character.PressureProtection = (float)value; return true; } } + case nameof(Character.SpeedMultiplier): + { if (parentObject is Character character) { character.StackSpeedMultiplier(value); return true; } } break; - case "LowPassMultiplier": - { if (parentObject is Character character && value is float) { character.LowPassMultiplier = (float)value; return true; } } - break; - case "SpeedMultiplier": - { if (parentObject is Character character && value is float) { character.StackSpeedMultiplier((float)value); return true; } } - break; - case "HealthMultiplier": - { if (parentObject is Character character && value is float) { character.StackHealthMultiplier((float)value); return true; } } - break; - case "IsOn": - { if (parentObject is LightComponent lightComponent && value is bool) { lightComponent.IsOn = (bool)value; return true; } } + case nameof(Character.HealthMultiplier): + { if (parentObject is Character character) { character.StackHealthMultiplier(value); return true; } } + break; + } + return false; + } + /// + /// Try setting the values of some commonly used properties directly without reflection + /// + private bool TrySetBoolValueWithoutReflection(object parentObject, bool value) + { + switch (Name) + { + case nameof(Character.ObstructVision): + { if (parentObject is Character character) { character.ObstructVision = value; return true; } } + break; + case nameof(LightComponent.IsOn): + { if (parentObject is LightComponent lightComponent) { lightComponent.IsOn = value; return true; } } + break; + case nameof(ItemComponent.IsActive): + { if (parentObject is ItemComponent ic) { ic.IsActive = value; return true; } } break; } - return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs index 7bd12a932..e6d051abf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/DelayedEffect.cs @@ -55,14 +55,23 @@ namespace Barotrauma public override void Apply(ActionType type, float deltaTime, Entity entity, ISerializableEntity target, Vector2? worldPosition = null) { if (this.type != type || !HasRequiredItems(entity)) { return; } - if (!Stackable && DelayList.Any(d => d.Parent == this && d.Targets.FirstOrDefault() == target)) { return; } + if (!Stackable) + { + foreach (var existingEffect in DelayList) + { + if (existingEffect.Parent == this && existingEffect.Targets.FirstOrDefault() == target) { return; } + } + } if (!IsValidTarget(target)) { return; } - if (!HasRequiredConditions(target.ToEnumerable())) { return; } + + currentTargets.Clear(); + currentTargets.Add(target); + if (!HasRequiredConditions(currentTargets)) { return; } switch (delayType) { case DelayTypes.Timer: - DelayList.Add(new DelayedListElement(this, entity, target.ToEnumerable(), delay, worldPosition, null)); + DelayList.Add(new DelayedListElement(this, entity, currentTargets, delay, worldPosition, null)); break; case DelayTypes.ReachCursor: Projectile projectile = (entity as Item)?.GetComponent(); @@ -78,16 +87,22 @@ namespace Barotrauma return; } - DelayList.Add(new DelayedListElement(this, entity, target.ToEnumerable(), Vector2.Distance(entity.WorldPosition, projectile.User.CursorWorldPosition), worldPosition, entity.WorldPosition)); + DelayList.Add(new DelayedListElement(this, entity, currentTargets, Vector2.Distance(entity.WorldPosition, projectile.User.CursorWorldPosition), worldPosition, entity.WorldPosition)); break; } } - public override void Apply(ActionType type, float deltaTime, Entity entity, IEnumerable targets, Vector2? worldPosition = null) + public override void Apply(ActionType type, float deltaTime, Entity entity, IReadOnlyList targets, Vector2? worldPosition = null) { if (this.type != type || !HasRequiredItems(entity)) { return; } - if (!Stackable && DelayList.Any(d => d.Parent == this && d.Targets.SequenceEqual(targets))) { return; } if (delayType == DelayTypes.ReachCursor && Character.Controlled == null) { return; } + if (!Stackable) + { + foreach (var existingEffect in DelayList) + { + if (existingEffect.Parent == this && existingEffect.Targets.SequenceEqual(targets)) { return; } + } + } currentTargets.Clear(); foreach (ISerializableEntity target in targets) diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs index 258bd5466..b73d3f280 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs @@ -178,15 +178,28 @@ namespace Barotrauma { if (target is Item item) { - return item.ContainedItems.Any(it => Matches(it)); + foreach (var containedItem in item.ContainedItems) + { + if (Matches(containedItem)) { return true; } + } + return false; } else if (target is Items.Components.ItemComponent ic) { - return ic.Item.ContainedItems.Any(it => Matches(it)); + foreach (var containedItem in ic.Item.ContainedItems) + { + if (Matches(containedItem)) { return true; } + } + return false; } else if (target is Character character) { - return character.Inventory != null && character.Inventory.AllItems.Any(it => Matches(it)); + if (character.Inventory == null) { return false; } + foreach (var containedItem in character.Inventory.AllItems) + { + if (Matches(containedItem)) { return true; } + } + return false; } } @@ -204,56 +217,34 @@ namespace Barotrauma if (target == null) { return Operator == OperatorType.NotEquals; } return (Operator == OperatorType.Equals) == (target.Name == AttributeValue); case ConditionType.HasTag: + if (target == null) { return Operator == OperatorType.NotEquals; } + return MatchesTagCondition(target); + case ConditionType.HasStatusTag: + if (target == null) { return Operator == OperatorType.NotEquals; } + int matches = 0; + foreach (DurationListElement durationEffect in StatusEffect.DurationList) { - if (target == null) { return Operator == OperatorType.NotEquals; } - int matches = 0; + if (!durationEffect.Targets.Contains(target)) { continue; } foreach (string tag in SplitAttributeValue) { - if (target is Item item && item.HasTag(tag)) + if (durationEffect.Parent.HasTag(tag)) { matches++; } } - //If operator is == then it needs to match everything, otherwise if its != there must be zero matches. - return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; } - case ConditionType.HasStatusTag: - if (target == null) { return Operator == OperatorType.NotEquals; } - bool success = false; - if (StatusEffect.DurationList.Any(d => d.Targets.Contains(target)) || DelayedEffect.DelayList.Any(d => d.Targets.Contains(target))) + foreach (DelayedListElement delayedEffect in DelayedEffect.DelayList) { - int matches = 0; - foreach (DurationListElement durationEffect in StatusEffect.DurationList) + if (!delayedEffect.Targets.Contains(target)) { continue; } + foreach (string tag in SplitAttributeValue) { - if (!durationEffect.Targets.Contains(target)) { continue; } - foreach (string tag in SplitAttributeValue) + if (delayedEffect.Parent.HasTag(tag)) { - if (durationEffect.Parent.HasTag(tag)) - { - matches++; - } - } - success = Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; - } - foreach (DelayedListElement delayedEffect in DelayedEffect.DelayList) - { - if (!delayedEffect.Targets.Contains(target)) { continue; } - foreach (string tag in SplitAttributeValue) - { - if (delayedEffect.Parent.HasTag(tag)) - { - matches++; - } + matches++; } } - return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; } - else if (Operator == OperatorType.NotEquals) - { - //no status effects, so the tags cannot be equal -> condition met - return true; - } - return success; + return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; case ConditionType.SpeciesName: { if (target == null) { return Operator == OperatorType.NotEquals; } @@ -335,96 +326,87 @@ namespace Barotrauma return false; } } + + private bool MatchesTagCondition(ISerializableEntity target) + { + if (!(target is Item item)) { return false; } + + int matches = 0; + foreach (string tag in SplitAttributeValue) + { + if (item.HasTag(tag)) + { + matches++; + } + } + //If operator is == then it needs to match everything, otherwise if its != there must be zero matches. + return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; + } + + public bool MatchesTagCondition(string targetTag) + { + if (string.IsNullOrEmpty(targetTag) || Type != ConditionType.HasTag) { return false; } + + int matches = 0; + foreach (string tag in SplitAttributeValue) + { + if (targetTag.Equals(tag, StringComparison.OrdinalIgnoreCase)) + { + matches++; + } + } + //If operator is == then it needs to match everything, otherwise if its != there must be zero matches. + return Operator == OperatorType.Equals ? matches >= SplitAttributeValue.Length : matches <= 0; + } // TODO: refactor and add tests private bool Matches(ISerializableEntity target, SerializableProperty property) { - object propertyValue = property.GetValue(target); + Type type = property.PropertyType; - if (propertyValue == null) + if (type == typeof(float) || type == typeof(int)) { - DebugConsole.ThrowError("Couldn't compare " + AttributeValue.ToString() + " (" + AttributeValue.GetType() + ") to property \"" + property.Name + "\" - property.GetValue() returns null!"); + float floatValue = property.GetFloatValue(target); + switch (Operator) + { + case OperatorType.Equals: + return MathUtils.NearlyEqual(floatValue, FloatValue.Value); + case OperatorType.NotEquals: + return !MathUtils.NearlyEqual(floatValue, FloatValue.Value); + case OperatorType.GreaterThan: + return floatValue > FloatValue.Value; + case OperatorType.LessThan: + return floatValue < FloatValue.Value; + case OperatorType.GreaterThanEquals: + return floatValue >= FloatValue.Value; + case OperatorType.LessThanEquals: + return floatValue <= FloatValue.Value; + } return false; } - Type type = propertyValue.GetType(); - float? floatProperty = null; - if (type == typeof(float) || type == typeof(int)) - { - floatProperty = (float)propertyValue; - } switch (Operator) { case OperatorType.Equals: if (type == typeof(bool)) { - return ((bool)propertyValue) == (AttributeValue == "true" || AttributeValue == "True"); - } - else if (FloatValue == null) - { - return propertyValue.ToString().Equals(AttributeValue); - } - else - { - return propertyValue.Equals(FloatValue); + return property.GetBoolValue(target) == (AttributeValue == "true" || AttributeValue == "True"); } + return property.GetValue(target).ToString().Equals(AttributeValue); + case OperatorType.NotEquals: if (type == typeof(bool)) { - return ((bool)propertyValue) != (AttributeValue == "true" || AttributeValue == "True"); - } - else if (FloatValue == null) - { - return !propertyValue.ToString().Equals(AttributeValue); - } - else - { - return !propertyValue.Equals(FloatValue); + return property.GetBoolValue(target) != (AttributeValue == "true" || AttributeValue == "True"); } + return !property.GetValue(target).ToString().Equals(AttributeValue); case OperatorType.GreaterThan: - if (FloatValue == null) - { - DebugConsole.ThrowError("Couldn't compare " + AttributeValue.ToString() + " (" + AttributeValue.GetType() + ") to property \"" + property.Name + "\" (" + type + ")! " - + "Make sure the type of the value set in the config files matches the type of the property."); - } - else if (floatProperty > FloatValue) - { - return true; - } - break; - case OperatorType.LessThan: - if (FloatValue == null) - { - DebugConsole.ThrowError("Couldn't compare " + AttributeValue.ToString() + " (" + AttributeValue.GetType() + ") to property \"" + property.Name + "\" (" + type + ")! " - + "Make sure the type of the value set in the config files matches the type of the property."); - } - else if (floatProperty < FloatValue) - { - return true; - } - break; - case OperatorType.GreaterThanEquals: - if (FloatValue == null) - { - DebugConsole.ThrowError("Couldn't compare " + AttributeValue.ToString() + " (" + AttributeValue.GetType() + ") to property \"" + property.Name + "\" (" + type + ")! " - + "Make sure the type of the value set in the config files matches the type of the property."); - } - else if (floatProperty >= FloatValue) - { - return true; - } - break; case OperatorType.LessThanEquals: - if (FloatValue == null) - { - DebugConsole.ThrowError("Couldn't compare " + AttributeValue.ToString() + " (" + AttributeValue.GetType() + ") to property \"" + property.Name + "\" (" + type + ")! " - + "Make sure the type of the value set in the config files matches the type of the property."); - } - else if (floatProperty <= FloatValue) - { - return true; - } + case OperatorType.LessThan: + case OperatorType.GreaterThanEquals: + DebugConsole.ThrowError("Couldn't compare " + AttributeValue.ToString() + " (" + AttributeValue.GetType() + ") to property \"" + property.Name + "\" (" + type + ")! " + + "Make sure the type of the value set in the config files matches the type of the property."); break; } return false; diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 43cd51e95..e0d34e7ef 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -217,7 +217,7 @@ namespace Barotrauma public string[] TalentIdentifiers; public bool GiveRandom; - public GiveTalentInfo(XElement element, string parentDebugName) + public GiveTalentInfo(XElement element, string _) { TalentIdentifiers = element.GetAttributeStringArray("talentidentifiers", new string[0], convertToLowerInvariant: true); GiveRandom = element.GetAttributeBool("giverandom", false); @@ -751,14 +751,41 @@ namespace Barotrauma for (int i = 0; i < propertyNames.Length; i++) { if (propertyNames[i] != "condition") { continue; } - if (propertyEffects[i].GetType() == typeof(float)) + object propertyEffect = propertyEffects[i]; + if (propertyEffect.GetType() == typeof(float)) { - return (float)propertyEffects[i] < 0.0f || (setValue && (float)propertyEffects[i] <= 0.0f); + return (float)propertyEffect < 0.0f || (setValue && (float)propertyEffect <= 0.0f); } } return false; } + public bool IncreasesItemCondition() + { + for (int i = 0; i < propertyNames.Length; i++) + { + if (propertyNames[i] != "condition") { continue; } + object propertyEffect = propertyEffects[i]; + if (propertyEffect.GetType() == typeof(float)) + { + return (float)propertyEffect > 0.0f || (setValue && (float)propertyEffect > 0.0f); + } + } + return false; + } + + public bool MatchesTagConditionals(ItemPrefab itemPrefab) + { + if (itemPrefab == null || !HasConditions) + { + return false; + } + else + { + return itemPrefab.Tags.Any(t => propertyConditionals.Any(pc => pc.MatchesTagCondition(t))); + } + } + public bool HasRequiredAfflictions(AttackResult attackResult) { if (requiredAfflictions == null) { return true; } @@ -787,7 +814,7 @@ namespace Barotrauma return true; } - public IEnumerable GetNearbyTargets(Vector2 worldPosition, List targets = null) + public IReadOnlyList GetNearbyTargets(Vector2 worldPosition, List targets = null) { targets ??= new List(); if (Range <= 0.0f) { return targets; } @@ -843,23 +870,24 @@ namespace Barotrauma } } - public bool HasRequiredConditions(IEnumerable targets) + public bool HasRequiredConditions(IReadOnlyList targets) { return HasRequiredConditions(targets, propertyConditionals); } - private bool HasRequiredConditions(IEnumerable targets, IEnumerable conditionals, bool targetingContainer = false) + private bool HasRequiredConditions(IReadOnlyList targets, IReadOnlyList conditionals, bool targetingContainer = false) { - if (conditionals.None()) { return true; } - if (requiredItems.Any() && requiredItems.All(ri => ri.MatchOnEmpty) && targets.None()) { return true; } + if (conditionals.Count == 0) { return true; } + if (targets.Count == 0 && requiredItems.Count > 0 && requiredItems.All(ri => ri.MatchOnEmpty)) { return true; } switch (conditionalComparison) { case PropertyConditional.Comparison.Or: - foreach (PropertyConditional pc in conditionals) + for (int i = 0; i < conditionals.Count; i++) { + var pc = conditionals[i]; if (pc.TargetContainer && !targetingContainer) { - var target = targets.FirstOrDefault(t => t is Item || t is ItemComponent); + var target = FindTargetItemOrComponent(targets); var targetItem = target as Item ?? (target as ItemComponent)?.Item; if (targetItem?.ParentInventory == null) { @@ -881,37 +909,28 @@ namespace Barotrauma if (pc.Type == PropertyConditional.ConditionType.HasTag) { //if we're checking for tags, just check the Item object, not the ItemComponents - if (HasRequiredConditions((container as ISerializableEntity).ToEnumerable(), pc.ToEnumerable(), targetingContainer: true)) { return true; } + if (pc.Matches(container)) { return true; } } else { - if (HasRequiredConditions(container.AllPropertyObjects, pc.ToEnumerable(), targetingContainer: true)) { return true; } + if (AnyTargetMatches(container.AllPropertyObjects, pc.TargetItemComponentName, pc)) { return true; } } } - if (owner is Character character && HasRequiredConditions(character.ToEnumerable(), pc.ToEnumerable(), targetingContainer: true)) { return true; } + if (owner is Character character && pc.Matches(character)) { return true; } } else { - foreach (ISerializableEntity target in targets) - { - if (!string.IsNullOrEmpty(pc.TargetItemComponentName)) - { - if (!(target is ItemComponent ic) || ic.Name != pc.TargetItemComponentName) - { - continue; - } - } - if (pc.Matches(target)) { return true; } - } + if (AnyTargetMatches(targets, pc.TargetItemComponentName, pc)) { return true; } } } return false; case PropertyConditional.Comparison.And: - foreach (PropertyConditional pc in conditionals) + for (int i = 0; i < conditionals.Count; i++) { + var pc = conditionals[i]; if (pc.TargetContainer && !targetingContainer) { - var target = targets.FirstOrDefault(t => t is Item || t is ItemComponent); + var target = FindTargetItemOrComponent(targets); var targetItem = target as Item ?? (target as ItemComponent)?.Item; if (targetItem?.ParentInventory == null) { @@ -933,29 +952,49 @@ namespace Barotrauma if (pc.Type == PropertyConditional.ConditionType.HasTag) { //if we're checking for tags, just check the Item object, not the ItemComponents - if (!HasRequiredConditions((container as ISerializableEntity).ToEnumerable(), pc.ToEnumerable(), targetingContainer: true)) { return false; } + if (!pc.Matches(container)) { return false; } } else { - if (!HasRequiredConditions(container.AllPropertyObjects, pc.ToEnumerable(), targetingContainer: true)) { return false; } + if (!AnyTargetMatches(container.AllPropertyObjects, pc.TargetItemComponentName, pc)) { return false; } } } - if (owner is Character character && !HasRequiredConditions(character.ToEnumerable(), pc.ToEnumerable(), targetingContainer: true)) { return false; } + if (owner is Character character && !pc.Matches(character)) { return false; } } else { - var validTargets = targets; - if (!string.IsNullOrEmpty(pc.TargetItemComponentName)) - { - validTargets = targets.Where(t => t is ItemComponent ic && ic.Name == pc.TargetItemComponentName); - } - if (targets.None(t => pc.Matches(t))) { return false; } + if (!AnyTargetMatches(targets, pc.TargetItemComponentName, pc)) { return false; } } } return true; default: throw new NotImplementedException(); } + + static bool AnyTargetMatches(IReadOnlyList targets, string targetItemComponentName, PropertyConditional conditional) + { + for (int i = 0; i < targets.Count; i++) + { + if (!string.IsNullOrEmpty(targetItemComponentName)) + { + if (!(targets[i] is ItemComponent ic) || ic.Name != targetItemComponentName) { continue; } + } + if (conditional.Matches(targets[i])) + { + return true; + } + } + return false; + } + + static ISerializableEntity FindTargetItemOrComponent(IReadOnlyList targets) + { + for (int i = 0; i < targets.Count; i++) + { + if (targets[i] is Item || targets[i] is ItemComponent) { return targets[i]; } + } + return null; + } } protected bool IsValidTarget(ISerializableEntity entity) @@ -972,14 +1011,27 @@ namespace Barotrauma { if (targetIdentifiers == null) { return true; } if (targetIdentifiers.Contains("structure")) { return true; } - if (targetIdentifiers.Any(id => id.Equals(structure.Prefab.Identifier, StringComparison.OrdinalIgnoreCase))) { return true; } + foreach (var id in targetIdentifiers) + { + if (id.Equals(structure.Prefab.Identifier, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } } else if (entity is Character character) { return IsValidTarget(character); } if (targetIdentifiers == null) { return true; } - return targetIdentifiers.Any(id => id.Equals(entity.Name, StringComparison.OrdinalIgnoreCase)); + foreach (var id in targetIdentifiers) + { + if (id.Equals(entity.Name, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + return false; } protected bool IsValidTarget(ItemComponent itemComponent) @@ -989,7 +1041,14 @@ namespace Barotrauma if (targetIdentifiers == null) { return true; } if (targetIdentifiers.Contains("itemcomponent")) { return true; } if (itemComponent.Item.HasTag(targetIdentifiers)) { return true; } - return targetIdentifiers.Any(id => id.Equals(itemComponent.Item.Prefab.Identifier, StringComparison.OrdinalIgnoreCase)); + foreach (var id in targetIdentifiers) + { + if (id.Equals(itemComponent.Item.Prefab.Identifier, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + return false; } protected bool IsValidTarget(Item item) @@ -999,7 +1058,14 @@ namespace Barotrauma if (targetIdentifiers == null) { return true; } if (targetIdentifiers.Contains("item")) { return true; } if (item.HasTag(targetIdentifiers)) { return true; } - return targetIdentifiers.Any(id => id.Equals(item.Prefab.Identifier, StringComparison.OrdinalIgnoreCase)); + foreach (var id in targetIdentifiers) + { + if (id.Equals(item.Prefab.Identifier, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + return false; } protected bool IsValidTarget(Character character) @@ -1008,7 +1074,14 @@ namespace Barotrauma if (OnlyOutside && character.CurrentHull != null) { return false; } if (targetIdentifiers == null) { return true; } if (targetIdentifiers.Contains("character")) { return true; } - return targetIdentifiers.Any(id => id.Equals(character.SpeciesName, StringComparison.OrdinalIgnoreCase)); + foreach (var id in targetIdentifiers) + { + if (id.Equals(character.SpeciesName, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + return false; } public void SetUser(Character user) @@ -1037,12 +1110,14 @@ namespace Barotrauma } } - if (!HasRequiredConditions(target.ToEnumerable())) { return; } - Apply(deltaTime, entity, target.ToEnumerable(), worldPosition); + currentTargets.Clear(); + currentTargets.Add(target); + if (!HasRequiredConditions(currentTargets)) { return; } + Apply(deltaTime, entity, currentTargets, worldPosition); } protected readonly List currentTargets = new List(); - public virtual void Apply(ActionType type, float deltaTime, Entity entity, IEnumerable targets, Vector2? worldPosition = null) + public virtual void Apply(ActionType type, float deltaTime, Entity entity, IReadOnlyList targets, Vector2? worldPosition = null) { if (this.type != type) { return; } @@ -1095,39 +1170,52 @@ namespace Barotrauma return hull; } - private Vector2 GetPosition(Entity entity, IEnumerable targets, Vector2? worldPosition = null) + private Vector2 GetPosition(Entity entity, IReadOnlyList targets, Vector2? worldPosition = null) { Vector2 position = worldPosition ?? (entity == null || entity.Removed ? Vector2.Zero : entity.WorldPosition); if (worldPosition == null) { - if (entity is Character character && !character.Removed && targetLimbs?.FirstOrDefault(l => l != LimbType.None) is LimbType limbType) + if (entity is Character character && !character.Removed && targetLimbs != null) { - Limb limb = character.AnimController.GetLimb(limbType); - if (limb != null && !limb.Removed) + foreach (var targetLimbType in targetLimbs) { - position = limb.WorldPosition; + Limb limb = character.AnimController.GetLimb(targetLimbType); + if (limb != null && !limb.Removed) + { + position = limb.WorldPosition; + break; + } + } + } + else if (HasTargetType(TargetType.Contained)) + { + for (int i = 0; i < targets.Count; i++) + { + if (targets[i] is Item targetItem) + { + position = targetItem.WorldPosition; + break; + } } } else { - if (targets.FirstOrDefault(t => t is Limb) is Limb targetLimb && !targetLimb.Removed) + for (int i = 0; i < targets.Count; i++) { - position = targetLimb.WorldPosition; - } - else if (HasTargetType(TargetType.Contained)) - { - if (targets.FirstOrDefault(t => t is Item) is Item targetItem) + if (targets[i] is Limb targetLimb && !targetLimb.Removed) { - position = targetItem.WorldPosition; + position = targetLimb.WorldPosition; + break; } } } + } position += Offset; return position; } - protected void Apply(float deltaTime, Entity entity, IEnumerable targets, Vector2? worldPosition = null) + protected void Apply(float deltaTime, Entity entity, IReadOnlyList targets, Vector2? worldPosition = null) { if (lifeTime > 0) { @@ -1137,58 +1225,68 @@ namespace Barotrauma Hull hull = GetHull(entity); Vector2 position = GetPosition(entity, targets, worldPosition); - foreach (ISerializableEntity serializableEntity in targets) + if (useItemCount > 0) { - if (!(serializableEntity is Item item)) { continue; } - - Character targetCharacter = targets.FirstOrDefault(t => t is Character character && !character.Removed) as Character; - if (targetCharacter == null) + Character useTargetCharacter = null; + Limb useTargetLimb = null; + for (int i = 0; i < targets.Count; i++) { - foreach (var target in targets) + if (targets[i] is Character character && !character.Removed) { - if (target is Limb limb && limb.character != null && !limb.character.Removed) - { - targetCharacter = ((Limb)target).character; - } + useTargetCharacter = character; + break; + } + else if (targets[i] is Limb limb && limb.character != null && !limb.character.Removed) + { + useTargetLimb = limb; + useTargetCharacter ??= limb.character; + break; } } - for (int i = 0; i < useItemCount; i++) + for (int i = 0; i < targets.Count; i++) { - if (item.Removed) { continue; } - item.Use(deltaTime, targetCharacter, targets.FirstOrDefault(t => t is Limb) as Limb); + if (!(targets[i] is Item item)) { continue; } + for (int j = 0; j < useItemCount; j++) + { + if (item.Removed) { continue; } + item.Use(deltaTime, useTargetCharacter, useTargetLimb); + } } } if (removeItem) { - foreach (var target in targets) + for (int i = 0; i < targets.Count; i++) { - if (target is Item item) { Entity.Spawner?.AddToRemoveQueue(item); } + if (targets[i] is Item item) { Entity.Spawner?.AddToRemoveQueue(item); } } } if (removeCharacter) { - foreach (var target in targets) + for (int i = 0; i < targets.Count; i++) { - if (target is Character character) { Entity.Spawner?.AddToRemoveQueue(character); } + if (targets[i] is Character character) { Entity.Spawner?.AddToRemoveQueue(character); } } } if (breakLimb || hideLimb) { - foreach (var target in targets) + for (int i = 0; i < targets.Count; i++) { - if (target is Character character) + if (targets[i] is Character character) { - var matchingLimb = character.AnimController.Limbs.FirstOrDefault(l => l.body == sourceBody); - if (matchingLimb != null) + foreach (Limb limb in character.AnimController.Limbs) { - if (breakLimb) + if (limb.body == sourceBody) { - character.TrySeverLimbJoints(matchingLimb, severLimbsProbability: 100, damage: 100, allowBeheading: true); - } - else - { - matchingLimb.HideAndDisable(hideLimbTimer); + if (breakLimb) + { + character.TrySeverLimbJoints(limb, severLimbsProbability: 100, damage: 100, allowBeheading: true, attacker: user); + } + else + { + limb.HideAndDisable(hideLimbTimer); + } + break; } } } @@ -1201,10 +1299,10 @@ namespace Barotrauma } else { - foreach (ISerializableEntity target in targets) + for (int i = 0; i < targets.Count; i++) { + var target = targets[i]; if (target == null) { continue; } - if (target is Entity targetEntity) { if (targetEntity.Removed) { continue; } @@ -1215,13 +1313,13 @@ namespace Barotrauma position = limb.WorldPosition + Offset; } - for (int i = 0; i < propertyNames.Length; i++) + for (int j = 0; j < propertyNames.Length; j++) { - if (target == null || target.SerializableProperties == null || !target.SerializableProperties.TryGetValue(propertyNames[i], out SerializableProperty property)) + if (target == null || target.SerializableProperties == null || !target.SerializableProperties.TryGetValue(propertyNames[j], out SerializableProperty property)) { continue; } - ApplyToProperty(target, property, propertyEffects[i], deltaTime); + ApplyToProperty(target, property, j, deltaTime); } } } @@ -1231,8 +1329,11 @@ namespace Barotrauma explosion.Explode(position, damageSource: entity, attacker: user); } - foreach (ISerializableEntity target in targets) + bool isNotClient = GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient; + + for (int i = 0; i < targets.Count; i++) { + var target = targets[i]; //if the effect has a duration, these will be done in the UpdateAll method if (duration > 0) { break; } if (target == null) { continue; } @@ -1251,7 +1352,7 @@ namespace Barotrauma if (limb.IsSevered) { continue; } if (targetLimbs != null && !targetLimbs.Contains(limb.type)) { continue; } AttackResult result = limb.character.DamageLimb(position, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source, allowStacking: !setValue); - limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, disableDeltaTime ? result.Damage : result.Damage / deltaTime, allowBeheading: true); + limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, disableDeltaTime ? result.Damage : result.Damage / deltaTime, allowBeheading: true, attacker: affliction.Source); RegisterTreatmentResults(entity, limb, affliction, result); //only apply non-limb-specific afflictions to the first limb if (!affliction.Prefab.LimbSpecific) { break; } @@ -1263,7 +1364,7 @@ namespace Barotrauma if (limb.character.Removed || limb.Removed) { continue; } newAffliction = GetMultipliedAffliction(affliction, entity, limb.character, deltaTime, multiplyAfflictionsByMaxVitality); AttackResult result = limb.character.DamageLimb(position, limb, newAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source, allowStacking: !setValue); - limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, disableDeltaTime ? result.Damage : result.Damage / deltaTime, allowBeheading: true); + limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability, disableDeltaTime ? result.Damage : result.Damage / deltaTime, allowBeheading: true, attacker: affliction.Source); RegisterTreatmentResults(entity, limb, affliction, result); } } @@ -1302,7 +1403,7 @@ namespace Barotrauma } } - if (aiTriggers.Any()) + if (aiTriggers.Count > 0) { Character targetCharacter = target as Character; if (targetCharacter == null) @@ -1328,7 +1429,7 @@ namespace Barotrauma } } - if (talentTriggers.Any()) + if (talentTriggers.Count > 0) { Character targetCharacter = CharacterFromTarget(target); if (targetCharacter != null && !targetCharacter.Removed) @@ -1337,14 +1438,12 @@ namespace Barotrauma { targetCharacter.CheckTalents(AbilityEffectType.OnStatusEffectIdentifier, new AbilityStatusEffectIdentifier(talentTrigger)); } - } } - if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) + if (isNotClient) { // these effects do not need to be run clientside, as they are replicated from server to clients anyway - foreach (int giveExperience in giveExperiences) { Character targetCharacter = CharacterFromTarget(target); @@ -1354,7 +1453,7 @@ namespace Barotrauma } } - if (giveSkills.Any()) + if (giveSkills.Count > 0) { foreach (GiveSkill giveSkill in giveSkills) { @@ -1373,7 +1472,7 @@ namespace Barotrauma } } - if (giveTalentInfos.Any()) + if (giveTalentInfos.Count > 0) { Character targetCharacter = CharacterFromTarget(target); if (targetCharacter?.Info == null) { continue; } @@ -1408,7 +1507,6 @@ namespace Barotrauma fire.Size = new Vector2(FireSize, fire.Size.Y); } - bool isNotClient = GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient; if (isNotClient && GameMain.GameSession?.EventManager is { } eventManager) { foreach (EventPrefab eventPrefab in triggeredEvents) @@ -1423,7 +1521,7 @@ namespace Barotrauma { List eventTargets = targets.Where(t => t is Entity).Cast().ToList(); - if (eventTargets.Any()) + if (eventTargets.Count > 0) { scriptedEvent.Targets.Add(triggeredEventTargetTag, eventTargets); } @@ -1616,17 +1714,19 @@ namespace Barotrauma } } - partial void ApplyProjSpecific(float deltaTime, Entity entity, IEnumerable targets, Hull currentHull, Vector2 worldPosition, bool playSound); + partial void ApplyProjSpecific(float deltaTime, Entity entity, IReadOnlyList targets, Hull currentHull, Vector2 worldPosition, bool playSound); - private void ApplyToProperty(ISerializableEntity target, SerializableProperty property, object value, float deltaTime) + private void ApplyToProperty(ISerializableEntity target, SerializableProperty property, int effectIndex, float deltaTime) { if (disableDeltaTime || setValue) { deltaTime = 1.0f; } - if (value is int || value is float) + object propertyEffect = propertyEffects[effectIndex]; + if (propertyEffect is int || propertyEffect is float) { - var propertyValue = property.GetValue(target); - if (propertyValue is float propertyValueF) + float propertyValueF = property.GetFloatValue(target); + if (property.PropertyType == typeof(float)) { - float floatValue = Convert.ToSingle(value) * deltaTime; + float floatValue = propertyEffect is float single ? single : (int)propertyEffect; + floatValue *= deltaTime; if (!setValue) { floatValue += propertyValueF; @@ -1634,18 +1734,23 @@ namespace Barotrauma property.TrySetValue(target, floatValue); return; } - else if (propertyValue is int integer) + else if (property.PropertyType == typeof(int)) { - int intValue = (int)(Convert.ToInt32(value) * deltaTime); + int intValue = (int)(propertyEffect is float single ? single * deltaTime : (int)propertyEffect * deltaTime); if (!setValue) { - intValue += integer; + intValue += (int)propertyValueF; } property.TrySetValue(target, intValue); return; } } - property.TrySetValue(target, value); + else if (propertyEffect is bool propertyValueBool) + { + property.TrySetValue(target, propertyValueBool); + return; + } + property.TrySetValue(target, propertyEffect); } public static void UpdateAll(float deltaTime) @@ -1682,7 +1787,7 @@ namespace Barotrauma { continue; } - element.Parent.ApplyToProperty(target, property, element.Parent.propertyEffects[n], CoroutineManager.UnscaledDeltaTime); + element.Parent.ApplyToProperty(target, property, n, CoroutineManager.UnscaledDeltaTime); } foreach (Affliction affliction in element.Parent.Afflictions) diff --git a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs index f05045e14..c6b96185c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs @@ -224,9 +224,9 @@ namespace Barotrauma public static void OnItemRepaired(Item item, Character fixer) { #if CLIENT - if (GameMain.Client != null) return; + if (GameMain.Client != null) { return; } #endif - if (fixer == null) return; + if (fixer == null) { return; } UnlockAchievement(fixer, "repairdevice"); UnlockAchievement(fixer, "repair" + item.Prefab.Identifier); @@ -234,10 +234,10 @@ namespace Barotrauma public static void OnAfflictionRemoved(Affliction affliction, Character character) { - if (string.IsNullOrEmpty(affliction.Prefab.AchievementOnRemoved)) return; + if (string.IsNullOrEmpty(affliction.Prefab.AchievementOnRemoved)) { return; } #if CLIENT - if (GameMain.Client != null) return; + if (GameMain.Client != null) { return; } #endif UnlockAchievement(character, affliction.Prefab.AchievementOnRemoved); } @@ -245,26 +245,29 @@ namespace Barotrauma public static void OnCharacterRevived(Character character, Character reviver) { #if CLIENT - if (GameMain.Client != null) return; + if (GameMain.Client != null) { return; } #endif - if (reviver == null) return; + if (reviver == null) { return; } UnlockAchievement(reviver, "healcrit"); } public static void OnCharacterKilled(Character character, CauseOfDeath causeOfDeath) { #if CLIENT - if (GameMain.Client != null || GameMain.GameSession == null) return; -#endif + if (GameMain.Client != null || GameMain.GameSession == null) { return; } if (character != Character.Controlled && causeOfDeath.Killer != null && causeOfDeath.Killer == Character.Controlled) { - SteamManager.IncrementStat( - character.IsHuman ? "humanskilled" : "monsterskilled", - 1); + IncrementStat(causeOfDeath.Killer, character.IsHuman ? "humanskilled" : "monsterskilled", 1); } +#elif SERVER + if (character != causeOfDeath.Killer && causeOfDeath.Killer != null) + { + IncrementStat(causeOfDeath.Killer, character.IsHuman ? "humanskilled" : "monsterskilled", 1); + } +#endif roundData?.Casualties.Add(character); @@ -337,13 +340,15 @@ namespace Barotrauma public static void OnTraitorWin(Character character) { #if CLIENT - if (GameMain.Client != null || GameMain.GameSession == null) return; + if (GameMain.Client != null || GameMain.GameSession == null) { return; } #endif UnlockAchievement(character, "traitorwin"); } public static void OnRoundEnded(GameSession gameSession) { + if (CheatsEnabled) { return; } + //made it to the destination if (gameSession?.Submarine != null && Level.Loaded != null && gameSession.Submarine.AtEndExit) { @@ -358,14 +363,14 @@ namespace Barotrauma !myCharacter.IsDead && (myCharacter.Submarine == gameSession.Submarine || (Level.Loaded?.EndOutpost != null && myCharacter.Submarine == Level.Loaded.EndOutpost))) { - SteamManager.IncrementStat("kmstraveled", levelLengthKilometers); + IncrementStat("kmstraveled", levelLengthKilometers); } #endif } else { //in sp making it to the end is enough - SteamManager.IncrementStat("kmstraveled", levelLengthKilometers); + IncrementStat("kmstraveled", levelLengthKilometers); } } @@ -461,42 +466,63 @@ namespace Barotrauma private static void UnlockAchievement(Character recipient, string identifier) { - if (CheatsEnabled) return; - if (recipient == null) return; + if (CheatsEnabled || recipient == null) { return; } #if CLIENT if (recipient == Character.Controlled) { UnlockAchievement(identifier); } -#endif -#if SERVER +#elif SERVER GameMain.Server?.GiveAchievement(recipient, identifier); #endif } - + + private static void IncrementStat(Character recipient, string identifier, int amount) + { + if (CheatsEnabled || recipient == null) { return; } +#if CLIENT + if (recipient == Character.Controlled) + { + SteamManager.IncrementStat(identifier, amount); + } +#elif SERVER + GameMain.Server?.IncrementStat(recipient, identifier, amount); +#endif + } + + public static void IncrementStat(string identifier, int amount) + { + if (CheatsEnabled) { return; } + SteamManager.IncrementStat(identifier, amount); + } + + public static void IncrementStat(string identifier, float amount) + { + if (CheatsEnabled) { return; } + SteamManager.IncrementStat(identifier, amount); + } + public static void UnlockAchievement(string identifier, bool unlockClients = false, Func conditions = null) { - if (CheatsEnabled) return; + if (CheatsEnabled) { return; } identifier = identifier.ToLowerInvariant(); #if SERVER - if (unlockClients && GameMain.Server != null) { foreach (Client c in GameMain.Server.ConnectedClients) { - if (conditions != null && !conditions(c.Character)) continue; + if (conditions != null && !conditions(c.Character)) { continue; } GameMain.Server.GiveAchievement(c, identifier); } } #endif - //already unlocked, no need to do anything - if (unlockedAchievements.Contains(identifier)) return; + if (unlockedAchievements.Contains(identifier)) { return; } unlockedAchievements.Add(identifier); #if CLIENT - if (conditions != null && !conditions(Character.Controlled)) return; + if (conditions != null && !conditions(Character.Controlled)) { return; } #endif SteamManager.UnlockAchievement(identifier); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs index 038c69e55..81af9005f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/IdRemap.cs @@ -7,7 +7,7 @@ using System.Xml.Linq; namespace Barotrauma { - public class IdRemap + public sealed class IdRemap { public static readonly IdRemap DiscardId = new IdRemap(null, -1); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs index 0621dd858..2fece0b4d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SafeIO.cs @@ -58,21 +58,37 @@ namespace Barotrauma.IO public static class SafeXML { - public static void SaveSafe(this System.Xml.Linq.XDocument doc, string path) + public static void SaveSafe(this System.Xml.Linq.XDocument doc, string path, bool throwExceptions = false) { if (!Validation.CanWrite(path, false)) { - DebugConsole.ThrowError($"Cannot save XML document to \"{path}\": modifying the files in this folder/with this extension is not allowed."); + string errorMsg = $"Cannot save XML document to \"{path}\": modifying the files in this folder/with this extension is not allowed."; + if (throwExceptions) + { + throw new InvalidOperationException(errorMsg); + } + else + { + DebugConsole.ThrowError(errorMsg); + } return; } doc.Save(path); } - public static void SaveSafe(this System.Xml.Linq.XElement element, string path) + public static void SaveSafe(this System.Xml.Linq.XElement element, string path, bool throwExceptions = false) { if (!Validation.CanWrite(path, false)) { - DebugConsole.ThrowError($"Cannot save XML element to \"{path}\": modifying the files in this folder/with this extension is not allowed."); + string errorMsg = $"Cannot save XML element to \"{path}\": modifying the files in this folder/with this extension is not allowed."; + if (throwExceptions) + { + throw new InvalidOperationException(errorMsg); + } + else + { + DebugConsole.ThrowError(errorMsg); + } return; } element.Save(path); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs index a797bde55..6e427d5ce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/SaveUtil.cs @@ -7,7 +7,8 @@ using System.Linq; using System.Text; using System.Threading; using System.Xml.Linq; -using Microsoft.Xna.Framework; +using Steamworks.Data; +using Color = Microsoft.Xna.Framework.Color; namespace Barotrauma { @@ -66,6 +67,7 @@ namespace Barotrauma catch (Exception e) { DebugConsole.ThrowError("Failed to clear folder", e); + return; } try @@ -75,6 +77,7 @@ namespace Barotrauma catch (Exception e) { DebugConsole.ThrowError("Error saving gamesession", e); + return; } try @@ -107,6 +110,7 @@ namespace Barotrauma catch (Exception e) { DebugConsole.ThrowError("Error saving submarine", e); + return; } try @@ -356,10 +360,10 @@ namespace Barotrauma private static bool DecompressFile(bool writeFile, string sDir, GZipStream zipStream, ProgressDelegate progress, out string fileName) { fileName = null; - + //Decompress file name byte[] bytes = new byte[sizeof(int)]; - int Readed = zipStream.Read(bytes, 0, sizeof(int)); + int Readed = Read(zipStream, bytes, sizeof(int)); if (Readed < sizeof(int)) return false; @@ -373,29 +377,29 @@ namespace Barotrauma StringBuilder sb = new StringBuilder(); for (int i = 0; i < iNameLen; i++) { - zipStream.Read(bytes, 0, sizeof(char)); + Read(zipStream, bytes, sizeof(char)); char c = BitConverter.ToChar(bytes, 0); sb.Append(c); } string sFileName = sb.ToString(); - + fileName = sFileName; progress?.Invoke(sFileName); //Decompress file content bytes = new byte[sizeof(int)]; - zipStream.Read(bytes, 0, sizeof(int)); + Read(zipStream, bytes, sizeof(int)); int iFileLen = BitConverter.ToInt32(bytes, 0); bytes = new byte[iFileLen]; - zipStream.Read(bytes, 0, bytes.Length); + Read(zipStream, bytes, bytes.Length); string sFilePath = Path.Combine(sDir, sFileName); string sFinalDir = Path.GetDirectoryName(sFilePath); string sDirFull = (string.IsNullOrEmpty(sDir) ? Directory.GetCurrentDirectory() : Path.GetFullPath(sDir)).CleanUpPathCrossPlatform(correctFilenameCase: false); string sFinalDirFull = (string.IsNullOrEmpty(sFinalDir) ? Directory.GetCurrentDirectory() : Path.GetFullPath(sFinalDir)).CleanUpPathCrossPlatform(correctFilenameCase: false); - + if (!sFinalDirFull.StartsWith(sDirFull, StringComparison.OrdinalIgnoreCase)) { throw new InvalidOperationException( @@ -427,6 +431,26 @@ namespace Barotrauma return true; } + private static int Read(GZipStream zipStream, byte[] bytes, int amount) + { + int read = 0; + + // FIXME workaround for .NET6 causing save decompression to fail +#if NET6_0 && LINUX + for (int i = 0; i < amount; i++) + { + int result = zipStream.ReadByte(); + if (result < 0) { break; } + + bytes[i] = (byte) result; + read++; + } +#else + read = zipStream.Read(bytes, 0, amount); +#endif + return read; + } + public static void DecompressToDirectory(string sCompressedFile, string sDir, ProgressDelegate progress) { DebugConsole.Log("Decompressing " + sCompressedFile + " to " + sDir + "..."); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskExtensions.cs new file mode 100644 index 000000000..1b4d14dda --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/TaskExtensions.cs @@ -0,0 +1,18 @@ +using System.Threading.Tasks; + +namespace Barotrauma +{ + static class TaskExtensions + { + public static bool TryGetResult(this Task task, out T result) + { + if (task is Task { IsCompletedSuccessfully: true } castTask) + { + result = castTask.Result; + return true; + } + result = default; + return false; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index 3c4e97c06..bc5aa7d5b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -535,16 +535,6 @@ namespace Barotrauma } } - // Enum.HasFlag() sucks - public static bool IsBitSet(this T self, T bit) where T : struct, Enum - { - // This uses Unsafe.As for performance reasons, as - // C# will otherwise not allow a T -> int cast - // without first casting to object, which would make - // this not any better than Enum.HasFlag - return (Unsafe.As(ref self) & Unsafe.As(ref bit)) != 0; - } - public static string ByteArrayToString(byte[] ba) { StringBuilder hex = new StringBuilder(ba.Length * 2); diff --git a/Barotrauma/BarotraumaShared/Submarines/Barsuk.sub b/Barotrauma/BarotraumaShared/Submarines/Barsuk.sub new file mode 100644 index 000000000..420580824 Binary files /dev/null and b/Barotrauma/BarotraumaShared/Submarines/Barsuk.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Berilia.sub b/Barotrauma/BarotraumaShared/Submarines/Berilia.sub index fc12f5627..e7e878fc7 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Berilia.sub and b/Barotrauma/BarotraumaShared/Submarines/Berilia.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Orca.sub b/Barotrauma/BarotraumaShared/Submarines/Orca.sub index 803ea52cb..97f6fa3cd 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Orca.sub and b/Barotrauma/BarotraumaShared/Submarines/Orca.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Orca2.sub b/Barotrauma/BarotraumaShared/Submarines/Orca2.sub index 744b20f4a..d9a6efe4e 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Orca2.sub and b/Barotrauma/BarotraumaShared/Submarines/Orca2.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/R-29.sub b/Barotrauma/BarotraumaShared/Submarines/R-29.sub index c35ec236e..e46392048 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/R-29.sub and b/Barotrauma/BarotraumaShared/Submarines/R-29.sub differ diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index cbb8356d3..02fc875fe 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,139 @@ +--------------------------------------------------------------------------------------------------------- +v0.16.0.0 +--------------------------------------------------------------------------------------------------------- + +Changes and additions: +- Added a medical clinic to outposts that allows you to heal your crew for a price. +- Added Barsuk, a small beginner-level sub. +- Added photoshop-like layers to submarine editor. +- Added some new decorative items and structures. +- Adjusted medical items' effects on bleeding and burns. Bandages, plastiseal and antibiotic glue are now much more effective at treating them, and morphine & fentanyl only heal them by a negligible amount. +- Heavily increased supercapacitor's power consumption and made the recharge speed increase exponentially when the recharge rate is increased. +- Optimizations to signal logic, status effects and property conditionals. +- Added "separator" property to Concat Component. +- Added "power_value_out" and "load_value_out" connections to junction boxes. +- Added "set_output" connection to Greater Than Component, Equals Component and Regex Component. +- Made alien materials spawn in abyss islands. +- Exposed wall healths in the sub editor. +- Modified Orca2 and R-29 reactor values so they're more in line with other subs. +- Made ballast flora toxins more visible and made them emit a sound. +- Health scanner doesn't show buffs from talents. +- Made impacts toss items around less effectively, especially when the item is heavy. +- Periscopes determine which turret to focus on using the trigger_out connection instead of position_out (making it easier to build circuits that switch which turret a periscope controls). +- Gave spinelings and threshers inventories (+ added alien blood as loot) to make it possible for "gene harvester" and "deep sea slayer" to spawn loot. +- Monsters' burns don't heal by themselves. +- "Allow rewiring" server setting doesn't affect wrecks, pirate subs or ruins. +- Added an option to disable all in-game hints to the hint message box. +- Option to make sonar displays center on the connected sonar transducer. +- Made the sizes of signal components consistent (32x32px, they also align to the grid now). +- Don't allow fabricators to take items from linked containers the user doesn't have access to. +- Connected diving suit lockers to oxygen in vanilla subs. + +AI: +- When quick-assigning orders, prioritize the characters with the same Operate order only when they are targeting the same item. +- When quick-assigning orders, don't prioritize characters with the same Maintenance order. Otherwise, Maintenance orders will always be quick-assigned to characters who already have the samer order. This will prevent giving out multiple Maintenance orders of the same type to multiple characters using the quick-assignment logic. +- Allow quick-assigning the same kind of Operate order to multiple characters. Previously, it would always be given to the character that already had the same kind of order. +- Fixed bots sometimes getting stuck while trying to fix a leak that's not in the same sub (e.g. bot in Remora and the leak in the drone). +- Fixed oxygen shards from old saves still being used as oxygen sources by bots. +- Fixed bots getting stuck in certain spots with ladders (e.g. Berilia's reactor room). +- Fixed contextual "clean up" order not being visible for weapons. +- Defined preferred containers for some items and added "locker" as the secondary preferred container for most items. Helps bots clean up things even when they can't find the primary container for the items. +- Fixed bots sometimes halting briefly next to a door when they shouldn't. +- "Fight intruders" order doesn't make the bots enter abandoned outposts to fight the enemies there. +- Disable aggressive behavior towards the player for the friendly crew members in single player (= accidental friendly fire never turns the security hostile in single player). +- Fixed bots saying they can't find items to load when someone takes an item they were targeting. +- Fixed bots saying they can't reach a leak when someone else fixes it before them. +- Fixed bots sometimes trying to adjust auto-controlled pumps when doing the autonomous Pump Water objective. +- Arrested pirate captains don't try to give orders to their crew. +- Change how captains (and theoretically other bots with an autonomous fight intruders order) behave: instead of idling around, they'll flee to the safety. And if there's no security officers around, they should fight the enemy aggressively. +- Fixed bots filling target containers with items that can't be refilled/recharged when given the Load Items order (e.g. putting welding fuel tanks in oxygen tank shelves). +- Made bots prefer the same fuel rods or ammo as already loaded when they're operating a reactor or a turret and need to find new ones. + +Bugfixes: +- Fixed equipping two of the same genetic material and then unequipping one of them removing all the genetic effects. +- Fixed "novice seafarer", "experienced seafarer" and "naval architect" achievements being possible to unlock even if cheats are enabled. +- Fixed kills in multiplayer sessions not progressing the "xenocide" and "genocide" achievements, and kills being reported to Steam unreliably. +- Fixed clients not spawning the respawn shuttle if they join after the server had disabled the shuttle mid-round, leading to an "entity not found" kick. +- Fixed medical effects being different when the medical item is fired with a syringe gun. +- Fixed fabricator sometimes desyncing in MP when some of the ingredients are in the user's inventory. +- Fixed clients trying to reconnect to SteamP2P indefinitely if establishing the initial connection fails, eventually leading to a crash. +- Fixed "allow linking wifi to chat" server setting causing syncing problems with headsets. The setting wasn't synced with clients who don't have settings management permissions, which would cause them to get some of the wifi components' properties mixed up and sometimes prevent them from communicating using the headsets. +- Fixed "unused talent points" indicator staying visible after all talents have been unlocked if the character's gained extra talent points from other talents. +- Fixed motion detector requiring the target's velocity to be higher than the specified minimum velocity, instead of higher than or equal. As a result, a minimum velocity of 0 would not sometimes detect targets in range. +- Fixed bots sometimes trying to put items they're cleaning up into containers inside fabricators/deconstructors (e.g. an oxygen tank into a diving suit in a fabricator). +- Fixed hammerhead matriarchs sometimes spawning in low-difficulty levels. +- Fixed bots equipping gene splicers when cleaning them up, causing the genetic material to get destroyed when the bot puts the splicer in a container. +- Fixed an exploit that allowed combining genetic materials in deconstructors. +- Fixed "fixitems" command setting genetic materials' condition to 100. +- Fixed "reset to prefab" not resetting wall healths. +- Fixed certain logic components not passing forwards the character who sent the signal, preventing e.g. the character who undocked a drone from being logged or the character who killed something from being determined if the signal activates a weapon. +- Orca 2: Fixed missing power wires to a couple small pumps, neutral ballast level, gunnery marked as wet room. Added a duct block between upper and lower deck. Some minor visual fixes. +- Fixed voronoi sites sometimes getting placed outside the level's bounds, leading to messed up level geometry. +- Fixed turrets always starting at rotation 0 at the beginning of the round (instead of halfway between the min/max angles like in the editor). +- Fixed incorrect talents sometimes unlocking server-side when unlocking "All-Seeing Eye". Happened because the server checked how many talents the client can unlock before applying All-Seeing Eye, which meant that the 3 extra talents would not be available, and the server would leave the last 3 talents unlocked. +- Fixed changing a delay component's delay using the editing hud not having an effect in-game when the component is receiving a continuous signal. +- Take structures/items with a collider into account when calculating a sub's dimensions (as opposed to just hulls). Fixes dimensions being incorrect in the submarine's info if the sub includes structures that extend far outside the hulls. +- Fixed crashing when swimming up from hull to another in a specific kind of hull configuration (two hulls side-by-side, with a gap leading up to another one). +- Fixed oxygen shards from old saves still being used as oxygen sources by bots. +- Fixed changes not being applied to all selected items when multi-editing a string field in the sub editor and deselecting the items without applying the changes by pressing enter. +- The game doesn't try to save a campaign if an exception occurs at any point during the saving process (should fix rare occurrences of campaign saves getting corrupted). +- Don't allow signals to deactivate ItemContainers. Fixes portable pumps' "toggle" input not working- +- Fixed "inspired to act" talent only giving a skill bonus of 9.98 instead of 10. +- Fixed removed items staying visible on the status monitor's electrical tab. +- Fixed plants still using the old values in old saves (i.e. dying too fast when not watered). +- Outposts can't request the "psychosisartifact_event" item (an event-specific special artifact that looks identical to the normal ones). +- Fixed size of a door's gap relative to the door changing when rescaling the door in the sub editor. +- Fixed fabricator consuming all the suitable ingredients when the ingredient is configured using a tag instead of an identifier (e.g. fabricating a stun gun dart would consume all the wires in the input slots). +- Fixed motion detector's detect offset getting mirrored when copying a mirrored detector. +- Fixed status monitor's submarine blueprint refreshing when initiating docking with a shuttle, instead of when the docking ports lock (sometimes causing the shuttle to appear slightly off from the docking port on the monito). +- Fixed fabricator failing to stack oxygenite tanks. +- Fixed "atmos machine" talent not spawning the alien pistol. +- Fixed items in the player's inventory not getting highlighted as valid ingredients when using a fabricator. +- Fixed fabricator failing to stack oxygenite tanks. +- Attempt to fix a rare crash caused by ScalableFont.DrawStringWithColors. +- Fixed freezing when trying to enable GameAnalytics from the settings menu on Mac. +- Fixed locked connection panel and non-interactable lights in R-29. +- Fixed Delay Component failing to parse set_delay inputs on systems that use comma as the decimal separator. +- Fixed charge rate not being displayed correctly on batteries in Chinese. +- Fixed junction box load not being displayed on status monitors in Russian. +- Fixed oxygen tanks being misaligned in oxygen generators. +- Fixed motion sensor not being able to detect subs in the sub editor test mode. +- Fixed recycled volatile fulgurium rods incorrectly using mechanical instead of electrical skill. +- Consider the character who severed a limb as the character who inflicted the afflictions caused by severing the limb. + Consider the character who caused bleeding as the character who caused the resulting bloodloss. Fixes achievements not unlocking if you kill a target by cutting its limbs off or by making it bleed to death. +- Fixed characters sometimes becoming momentarily unresponsive when swimming out from a hull. +- Fixed speed penalty caused by the vegetation in caves sometimes not disappearing after passing through the vegetation. +- Fixed links from a docking port to a linked sub not being considered valid in the sub editor (only a link from linked sub to a docking port). Now the order of the link doesn't matter. +- Fixed repair window showing up if you use a periscope wired to a broken device. +- Fixed sonar getting misaligned when switching to the docking mode (the amount of misalignment being relative to the distance of the docking port from the sub's center). +- Fixed "hazardous materials" considering any reactor outside the main sub a wreck reactor. +- Fixed light textures not rotating with the lamps in the sub editor. +- Fixed elements in CustomInterface getting misaligned if the signal_out connections aren't used in sequential order (e.g. if you only connect a wire to outputs 2 and 3). +- Fixed server including lines multiple times in the saved server logs (e.g. the 2nd saved log file would include some lines that were already saved to the 1st log file). +- Fixed ranged weapons (including turrets) triggering electrochemist's stun. +- Fixed initial husk infection message being displayed immediately after getting infected, not after the infection advances. +- Fixed equip slots being misplaced if you open the health interface when the equip slots have been hidden. +- Fixed wrecks sometimes not spawning in levels despite a wreck mission being selected. +- Fixed characters moving slowly downwards when aiming underwater. +- Fixed moloch shell shields not protecting the user from non-hitscan weapons or melee weapons. +- Fixed messed up mining crane sprite. +- Fixed crashing when pirates try to operate the sub using a nav terminal that doesn't control any sub (doesn't affect vanilla subs because they don't contain that kind of nav terminals). +- Fixed fabricator showing the info of the selected item wrong when selecting the fabricator with another character (e.g. fabrication time still calculated based on the previous user's skills). +- Fixed characters reading skillbooks upside-down. +- Fixed personality traits changing after every round in mp campaign. +- Fixed monsters always eating the character they're grabbing, even when the monster is configured as not being able to eat (in practice only happened when a player controlled something like a fractal guardian and grabbed another character). +- Fixed characters sometimes using the "priorities have changed" dialogue when giving a new order. +- Fixed pumps' auto-controlled status not being updated correctly. + +Modding: +- Added "HealCostMultiplier" attribute to AfflictionPrefabs that adjusts the heal cost in medical clinic. +- EntitySpawnerComponent treats positive y offset as up to make it more consistent with other components. +- Added an option to define a hard limit for how many entities EntitySpawnerComponent can spawn. +- Fixed "targetself" attack conditionals checking both the attacker and the target. +- Added "delaybetweenspawns" property to MonsterEvents (determines the delay between spawning the individual monsters of a given monster event). +- Don't allow setting an item's or limb's density to 0 (leads to "attempted to apply invalid force/torque" errors). +- Fixed shields blocking projectiles from the user's weapon. Didn't affect any vanilla items, because all the shields are 2-hand items that prevent using a weapon at the same time. +- Fixed ButtonTerminals without an ItemContainer component causing crashes. + --------------------------------------------------------------------------------------------------------- v0.15.23.0 --------------------------------------------------------------------------------------------------------- diff --git a/README.md b/README.md index 28678353b..99597222e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Barotrauma -Copyright © FakeFish Ltd 2017-2021 +Copyright © FakeFish Ltd 2017-2022 Before downloading the source code, please read the [EULA](EULA.txt).