diff --git a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs index 1afe18bd0..48648f15b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Camera.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Camera.cs @@ -335,7 +335,8 @@ namespace Barotrauma //an ad-hoc way of allowing the players to have roughly the same maximum view distance regardless of the resolution, //while still keeping the zoom around 1.0 when not looking further away (because otherwise we'd always be downsampling //on lower resolutions, which doesn't look that good) - float newZoom = MathHelper.Lerp(unscaledZoom, scaledZoom, (float)Math.Sqrt(zoomOutAmount)); + float newZoom = MathHelper.Lerp(unscaledZoom, scaledZoom, + (GameMain.Config == null || GameMain.Config.EnableMouseLook) ? (float)Math.Sqrt(zoomOutAmount) : 0.3f); Zoom += (newZoom - zoom) / ZoomSmoothness; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/AITarget.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/AITarget.cs index bf6555d5d..a1be8d950 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/AITarget.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/AITarget.cs @@ -44,7 +44,7 @@ namespace Barotrauma else { //color = Color.WhiteSmoke; - // disable the indicators for structures, because they clutter the debug view + // disable the indicators for structures and hulls, because they clutter the debug view return; } ShapeExtensions.DrawCircle(spriteBatch, pos, SightRange, 100, color, thickness: 1 / Screen.Selected.Cam.Zoom); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs new file mode 100644 index 000000000..fd9e93f7e --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/AI/Wreck/WreckAI.cs @@ -0,0 +1,50 @@ +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using Barotrauma.Networking; + +namespace Barotrauma +{ + partial class WreckAI : IServerSerializable + { + private CoroutineHandle fadeOutRoutine; + partial void FadeOutColors() + { + if (fadeOutRoutine != null) + { + CoroutineManager.StopCoroutines(fadeOutRoutine); + } + fadeOutRoutine = CoroutineManager.StartCoroutine(FadeOutColors(Config.DeadEntityColorFadeOutTime)); + } + + private IEnumerable FadeOutColors(float time) + { + float timer = 0; + while (timer < time) + { + timer += CoroutineManager.DeltaTime; + float m = MathHelper.Lerp(1, Config.DeadEntityColorMultiplier, MathUtils.InverseLerp(0, time, timer)); + foreach (var item in thalamusItems) + { + if (item.Prefab.BrokenSprites.None()) + { + Color c = item.prefab.SpriteColor; + item.SpriteColor = new Color(c.R / 255f * m, c.G / 255f * m, c.B / 255f * m, c.A / 255f); + } + } + foreach (var structure in thalamusStructures) + { + Color c = structure.prefab.SpriteColor; + structure.SpriteColor = new Color(c.R / 255f * m, c.G / 255f * m, c.B / 255f * m, c.A / 255f); + } + yield return CoroutineStatus.Running; + } + yield return CoroutineStatus.Success; + } + + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + { + IsAlive = msg.ReadBoolean(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs index 63d963175..eb622fe34 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs @@ -286,7 +286,7 @@ namespace Barotrauma { bool inWater = limb.inWater; if (character.CurrentHull != null && - character.CurrentHull.Surface > character.CurrentHull.Rect.Y - character.CurrentHull.Rect.Height && + character.CurrentHull.Surface > character.CurrentHull.Rect.Y - character.CurrentHull.Rect.Height + 5.0f && limb.SimPosition.Y < ConvertUnits.ToSimUnits(character.CurrentHull.Rect.Y - character.CurrentHull.Rect.Height) + limb.body.GetMaxExtent()) { inWater = true; @@ -370,6 +370,7 @@ namespace Barotrauma foreach (var deformation in SpriteDeformations) { if (character.IsDead && deformation.Params.StopWhenHostIsDead) { continue; } + if (!character.AnimController.InWater && deformation.Params.OnlyInWater) { continue; } if (deformation.Params.UseMovementSine) { if (this is AnimController animator) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index bdf4e44c8..5ec765476 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -233,7 +233,7 @@ namespace Barotrauma pressureParticleTimer += pressure * deltaTime; if (pressureParticleTimer > 10.0f) { - Particle p = GameMain.ParticleManager.CreateParticle("waterblood", WorldPosition + Rand.Vector(5.0f), Rand.Vector(10.0f)); + GameMain.ParticleManager.CreateParticle(Params.BleedParticleWater, WorldPosition + Rand.Vector(5.0f), Rand.Vector(10.0f)); pressureParticleTimer = 0.0f; } } @@ -355,7 +355,7 @@ namespace Barotrauma } } - partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult) + partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult, float stun) { if (attackResult.Damage <= 1.0f || IsDead) { return; } if (soundTimer < soundInterval * 0.5f) @@ -365,7 +365,7 @@ namespace Barotrauma } } - partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction) + partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool log) { if (GameMain.NetworkMember != null && controlled == this) { @@ -444,6 +444,7 @@ namespace Barotrauma if (item.body != null && !item.body.Enabled) continue; if (item.ParentInventory != null) continue; if (ignoredItems != null && ignoredItems.Contains(item)) continue; + if (Screen.Selected is SubEditorScreen editor && editor.WiringMode && item.GetComponent() == null) { continue; } if (draggingItemToWorld) { @@ -466,7 +467,7 @@ namespace Barotrauma //modify the distance based on the size of the trigger (preferring smaller items) distanceToItem *= MathHelper.Lerp(0.05f, 2.0f, (transformedTrigger.Width + transformedTrigger.Height) / 250.0f); } - else + else if (!item.Prefab.RequireCursorInsideTrigger) { Rectangle itemDisplayRect = new Rectangle(item.InteractionRect.X, item.InteractionRect.Y - item.InteractionRect.Height, item.InteractionRect.Width, item.InteractionRect.Height); @@ -551,7 +552,7 @@ namespace Barotrauma { if (!enabled) { return; } - if (!IsDead && !IsUnconscious) + if (!IsDead && !IsIncapacitated) { if (soundTimer > 0) { @@ -603,6 +604,11 @@ namespace Barotrauma } } + partial void SetOrderProjSpecific(Order order, string orderOption) + { + GameMain.GameSession?.CrewManager?.DisplayCharacterOrder(this, order, orderOption); + } + public static void AddAllToGUIUpdateList() { for (int i = 0; i < CharacterList.Count; i++) @@ -812,6 +818,7 @@ namespace Barotrauma { if (sounds == null || sounds.Count == 0) { return; } if (soundChannel != null && soundChannel.IsPlaying) { return; } + if (GameMain.SoundManager?.Disabled ?? true) { return; } var matchingSounds = sounds.Where(s => s.Type == soundType && @@ -820,6 +827,7 @@ namespace Barotrauma var matchingSoundsList = matchingSounds.ToList(); var selectedSound = matchingSoundsList[Rand.Int(matchingSoundsList.Count)]; + if (selectedSound?.Sound == null) { return; } soundChannel = SoundPlayer.PlaySound(selectedSound.Sound, AnimController.WorldPosition, selectedSound.Volume, selectedSound.Range, CurrentHull); soundTimer = soundInterval; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index d73a9aaef..f051a3b79 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -2,6 +2,7 @@ using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; using System.Linq; @@ -37,6 +38,9 @@ namespace Barotrauma } } + private static bool shouldRecreateHudTexts = true; + private static bool heldDownShiftWhenGotHudTexts; + private static bool ShouldDrawInventory(Character character) { return @@ -61,7 +65,7 @@ namespace Barotrauma { if (GUI.DisableHUD) return; - if (!character.IsUnconscious && character.Stun <= 0.0f) + if (!character.IsIncapacitated && character.Stun <= 0.0f) { if (character.Inventory != null) { @@ -90,7 +94,7 @@ namespace Barotrauma { if (GUI.DisableHUD) { return; } - if (!character.IsUnconscious && character.Stun <= 0.0f) + if (!character.IsIncapacitated && character.Stun <= 0.0f) { if (character.Info != null && !character.ShouldLockHud() && character.SelectedCharacter == null) { @@ -140,7 +144,11 @@ namespace Barotrauma else { focusedItemOverlayTimer = Math.Max(focusedItemOverlayTimer - deltaTime, 0.0f); - if (focusedItemOverlayTimer <= 0.0f) focusedItem = null; + if (focusedItemOverlayTimer <= 0.0f) + { + focusedItem = null; + shouldRecreateHudTexts = true; + } } } @@ -154,6 +162,7 @@ namespace Barotrauma brokenItemsCheckTimer = 1.0f; foreach (Item item in Item.ItemList) { + if (item.Submarine == null || item.Submarine.TeamID != character.TeamID || item.Submarine.Info.IsWreck) { continue; } if (!item.Repairables.Any(r => item.ConditionPercentage <= r.AIRepairThreshold)) { continue; } if (Submarine.VisibleEntities != null && !Submarine.VisibleEntities.Contains(item)) { continue; } @@ -201,27 +210,27 @@ namespace Barotrauma Color.Lerp(GUI.Style.Red, GUI.Style.Orange * 0.5f, brokenItem.Condition / brokenItem.MaxCondition) * alpha); } - if (!character.IsUnconscious && character.Stun <= 0.0f) + if (!character.IsIncapacitated && character.Stun <= 0.0f) { if (character.FocusedCharacter != null && character.FocusedCharacter.CanBeSelected) { DrawCharacterHoverTexts(spriteBatch, cam, character); } - float circleSize; if (character.FocusedItem != null) { if (focusedItem != character.FocusedItem) { focusedItemOverlayTimer = Math.Min(1.0f, focusedItemOverlayTimer); + shouldRecreateHudTexts = true; } - focusedItem = character.FocusedItem; + focusedItem = character.FocusedItem; } if (focusedItem != null && focusedItemOverlayTimer > ItemOverlayDelay) { Vector2 circlePos = cam.WorldToScreen(focusedItem.DrawPosition); - circleSize = Math.Max(focusedItem.Rect.Width, focusedItem.Rect.Height) * 1.5f; + float circleSize = Math.Max(focusedItem.Rect.Width, focusedItem.Rect.Height) * 1.5f; circleSize = MathHelper.Clamp(circleSize, 45.0f, 100.0f) * Math.Min((focusedItemOverlayTimer - 1.0f) * 5.0f, 1.0f); if (circleSize > 0.0f) { @@ -237,7 +246,14 @@ namespace Barotrauma if (!GUI.DisableItemHighlights && !Inventory.DraggingItemToWorld) { - var hudTexts = focusedItem.GetHUDTexts(character); + bool shiftDown = PlayerInput.KeyDown(Keys.LeftShift) || PlayerInput.KeyDown(Keys.RightShift); + if(shouldRecreateHudTexts || heldDownShiftWhenGotHudTexts != shiftDown) + { + shouldRecreateHudTexts = true; + heldDownShiftWhenGotHudTexts = shiftDown; + } + var hudTexts = focusedItem.GetHUDTexts(character, shouldRecreateHudTexts); + shouldRecreateHudTexts = false; int dir = Math.Sign(focusedItem.WorldPosition.X - character.WorldPosition.X); @@ -309,7 +325,7 @@ namespace Barotrauma (int)(HUDLayoutSettings.BottomRightInfoArea.Y + HUDLayoutSettings.BottomRightInfoArea.Height * 0.1f), (int)(HUDLayoutSettings.BottomRightInfoArea.Width / 2), (int)(HUDLayoutSettings.BottomRightInfoArea.Height * 0.7f))); - character.Info.DrawPortrait(spriteBatch, HUDLayoutSettings.PortraitArea.Location.ToVector2(), new Vector2((int)(-4 * GUI.Scale), (int)(2 * GUI.Scale)), targetWidth: HUDLayoutSettings.PortraitArea.Width, true); + character.Info.DrawPortrait(spriteBatch, HUDLayoutSettings.PortraitArea.Location.ToVector2(), new Vector2(-12 * GUI.Scale, 4 * GUI.Scale), targetWidth: HUDLayoutSettings.PortraitArea.Width, true); } mouseOnPortrait = HUDLayoutSettings.BottomRightInfoArea.Contains(PlayerInput.MousePosition) && !character.ShouldLockHud(); if (mouseOnPortrait) @@ -327,7 +343,7 @@ namespace Barotrauma } } - if (!character.IsUnconscious && character.Stun <= 0.0f) + if (!character.IsIncapacitated && character.Stun <= 0.0f) { if (character.IsHumanoid && character.SelectedCharacter != null && character.SelectedCharacter.Inventory != null) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs index 9758686d7..8058762f4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterInfo.cs @@ -10,84 +10,116 @@ namespace Barotrauma { partial class CharacterInfo { - public const float BgScale = 1.2f; private static Sprite infoAreaPortraitBG; public static void Init() { - infoAreaPortraitBG = new Sprite("Content/UI/InventoryUIAtlas.png", new Rectangle(833, 298, 142, 98), null, 0); + infoAreaPortraitBG = GUI.Style.GetComponentStyle("InfoAreaPortraitBG")?.Sprites[GUIComponent.ComponentState.None][0].Sprite; + new Sprite("Content/UI/InventoryUIAtlas.png", new Rectangle(833, 298, 142, 98), null, 0); } - public GUIFrame CreateInfoFrame(GUIFrame frame) + public GUIComponent CreateInfoFrame(GUIFrame frame, bool returnParent, Sprite permissionIcon = null) { - var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), frame.RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.1f) }) + var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.874f, 0.58f), frame.RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.05f) }) { - Stretch = true, - RelativeSpacing = 0.03f + RelativeSpacing = 0.05f + //Stretch = true }; - var headerArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.4f), paddedFrame.RectTransform), isHorizontal: true) - { - RelativeSpacing = 0.05f, - Stretch = true - }; + var headerArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.322f), paddedFrame.RectTransform), isHorizontal: true); - new GUICustomComponent(new RectTransform(new Vector2(0.25f, 1.0f), headerArea.RectTransform), - onDraw: (sb, component) => DrawIcon(sb, component.Rect.Center.ToVector2(), targetAreaSize: component.Rect.Size.ToVector2())); + new GUICustomComponent(new RectTransform(new Vector2(0.425f, 1.0f), headerArea.RectTransform), + onDraw: (sb, component) => DrawInfoFrameCharacterIcon(sb, component.Rect)); ScalableFont font = paddedFrame.Rect.Width < 280 ? GUI.SmallFont : GUI.Font; - var headerTextArea = new GUILayoutGroup(new RectTransform(new Vector2(0.75f, 1.0f), headerArea.RectTransform)) + var headerTextArea = new GUILayoutGroup(new RectTransform(new Vector2(0.575f, 1.0f), headerArea.RectTransform)) { - RelativeSpacing = 0.05f, + RelativeSpacing = 0.02f, Stretch = true }; Color? nameColor = null; if (Job != null) { nameColor = Job.Prefab.UIColor; } - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), - Name, textColor: nameColor, font: GUI.LargeFont) + + GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), ToolBox.LimitString(Name, GUI.Font, headerTextArea.Rect.Width), textColor: nameColor, font: GUI.Font) { - Padding = Vector4.Zero, - AutoScaleHorizontal = true + ForceUpperCase = true, + Padding = Vector4.Zero }; - if (Job != null) + if (permissionIcon != null) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), - Job.Name, textColor: Job.Prefab.UIColor, font: font); + Point iconSize = permissionIcon.SourceRect.Size; + int iconWidth = (int)((float)characterNameBlock.Rect.Height / iconSize.Y * iconSize.X); + new GUIImage(new RectTransform(new Point(iconWidth, characterNameBlock.Rect.Height), characterNameBlock.RectTransform) { AbsoluteOffset = new Point(-iconWidth - 2, 0) }, permissionIcon) { IgnoreLayoutGroups = true }; + } + + if (Job != null) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), Job.Name, textColor: Job.Prefab.UIColor, font: font) + { + Padding = Vector4.Zero + }; } if (personalityTrait != null) { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), - TextManager.AddPunctuation(':', TextManager.Get("PersonalityTrait"), TextManager.Get("personalitytrait." + personalityTrait.Name.Replace(" ", ""))), font: font); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), TextManager.AddPunctuation(':', TextManager.Get("PersonalityTrait"), TextManager.Get("personalitytrait." + personalityTrait.Name.Replace(" ", ""))), font: font) + { + Padding = Vector4.Zero + }; } - //spacing - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), paddedFrame.RectTransform), style: null); - - if (Job != null) + if (Job != null && (Character == null || !Character.IsDead)) { + var skillsArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.63f), paddedFrame.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter)) + { + Stretch = true + }; + var skills = Job.Skills; skills.Sort((s1, s2) => -s1.Level.CompareTo(s2.Level)); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), - TextManager.Get("Skills") + ":", font: font); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillsArea.RectTransform), TextManager.AddPunctuation(':', TextManager.Get("skills"), string.Empty), font: font) { Padding = Vector4.Zero }; foreach (Skill skill in skills) { Color textColor = Color.White * (0.5f + skill.Level / 200.0f); - var skillName = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform), - TextManager.Get("SkillName." + skill.Identifier), textColor: textColor, font: font); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), skillName.RectTransform), - ((int)skill.Level).ToString(), textColor: textColor, font: font, textAlignment: Alignment.CenterRight); + var skillName = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillsArea.RectTransform), TextManager.Get("SkillName." + skill.Identifier), textColor: textColor, font: font) { Padding = Vector4.Zero }; + new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), skillName.RectTransform), ((int)skill.Level).ToString(), textColor: textColor, font: font, textAlignment: Alignment.CenterRight); } } + else if (Character != null && Character.IsDead) + { + var deadArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.63f), paddedFrame.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter)) + { + Stretch = true + }; - return frame; + string deadDescription = TextManager.AddPunctuation(':', TextManager.Get("deceased") + "\n" + Character.CauseOfDeath.Affliction?.CauseOfDeathDescription ?? + TextManager.AddPunctuation(':', TextManager.Get("CauseOfDeath"), TextManager.Get("CauseOfDeath." + Character.CauseOfDeath.Type.ToString()))); + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), deadArea.RectTransform), deadDescription, textColor: GUI.Style.Red, font: font, textAlignment: Alignment.TopLeft) { Padding = Vector4.Zero }; + } + + if (returnParent) + { + return frame; + } + else + { + return paddedFrame; + } + } + + private void DrawInfoFrameCharacterIcon(SpriteBatch sb, Rectangle componentRect) + { + Vector2 targetAreaSize = componentRect.Size.ToVector2(); + float scale = Math.Min(targetAreaSize.X / headSprite.size.X, targetAreaSize.Y / headSprite.size.Y); + DrawIcon(sb, componentRect.Location.ToVector2() + headSprite.size / 2 * scale, targetAreaSize); } public GUIFrame CreateCharacterFrame(GUIComponent parent, string text, object userData) @@ -171,6 +203,7 @@ namespace Barotrauma public void DrawBackground(SpriteBatch spriteBatch) { + if (infoAreaPortraitBG == null) { return; } infoAreaPortraitBG.Draw(spriteBatch, HUDLayoutSettings.BottomRightInfoArea.Location.ToVector2(), Color.White, Vector2.Zero, 0.0f, scale: new Vector2( HUDLayoutSettings.BottomRightInfoArea.Width / (float)infoAreaPortraitBG.SourceRect.Width, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 28ca04741..56296974c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -1,4 +1,5 @@ -using Barotrauma.Networking; +using Barotrauma.Extensions; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Linq; @@ -277,7 +278,7 @@ namespace Barotrauma break; case ServerNetObject.ENTITY_EVENT: - int eventType = msg.ReadRangedInteger(0, 3); + int eventType = msg.ReadRangedInteger(0, 4); switch (eventType) { case 0: @@ -337,6 +338,36 @@ namespace Barotrauma info?.SetSkillLevel(skillIdentifier, skillLevel, WorldPosition + Vector2.UnitY * 150.0f); } break; + case 4: + int attackLimbIndex = msg.ReadByte(); + UInt16 targetEntityID = msg.ReadUInt16(); + int targetLimbIndex = msg.ReadByte(); + + if (attackLimbIndex >= AnimController.Limbs.Length) + { + DebugConsole.ThrowError($"Received invalid ExecuteAttack message. Limb index out of bounds ({attackLimbIndex})"); + break; + } + Limb attackLimb = AnimController.Limbs[attackLimbIndex]; + IDamageable targetEntity = FindEntityByID(targetEntityID) as IDamageable; + Limb targetLimb = null; + if (targetEntity == null) + { + DebugConsole.ThrowError($"Received invalid ExecuteAttack message. Target entity not found (ID {targetEntityID})"); + break; + } + if (targetEntity is Character targetCharacter) + { + if (targetLimbIndex >= targetCharacter.AnimController.Limbs.Length) + { + DebugConsole.ThrowError($"Received invalid ExecuteAttack message. Target limb index out of bounds ({targetLimbIndex})"); + break; + } + targetLimb = targetCharacter.AnimController.Limbs[targetLimbIndex]; + } + + attackLimb.ExecuteAttack(targetEntity, targetLimb, out _); + break; } msg.ReadPadBits(); break; @@ -384,6 +415,36 @@ namespace Barotrauma character = Create(speciesName, position, seed, info, GameMain.Client.ID != ownerId, hasAi); character.ID = id; character.TeamID = (TeamType)teamID; + + // Check if the character has a current order + if (inc.ReadBoolean()) + { + int orderPrefabIndex = inc.ReadByte(); + Entity targetEntity = FindEntityByID(inc.ReadUInt16()); + Character orderGiver = inc.ReadBoolean() ? FindEntityByID(inc.ReadUInt16()) as Character : null; + int orderOptionIndex = inc.ReadByte(); + + if (orderPrefabIndex >= 0 && orderPrefabIndex < Order.PrefabList.Count) + { + var orderPrefab = Order.PrefabList[orderPrefabIndex]; + if (!orderPrefab.MustSetTarget || (targetEntity != null && (targetEntity as Item).Components.Any(c => c?.GetType() == orderPrefab.ItemComponentType))) + { + character.SetOrder( + new Order(orderPrefab, targetEntity, (targetEntity as Item)?.Components.FirstOrDefault(c => c?.GetType() == orderPrefab.ItemComponentType), orderGiver: orderGiver), + orderOptionIndex >= 0 && orderOptionIndex < orderPrefab.Options.Length ? orderPrefab.Options[orderOptionIndex] : null, + orderGiver, speak: false); + } + else + { + DebugConsole.ThrowError("Could not set order \"" + orderPrefab.Identifier + "\" for character \"" + character.Name + "\" because required target entity was not found."); + } + } + else + { + DebugConsole.ThrowError("Invalid order prefab index - index (" + orderPrefabIndex + ") out of bounds."); + } + } + bool containsStatusData = inc.ReadBoolean(); if (containsStatusData) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterSound.cs index a096c3cc9..e93c6e2b8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterSound.cs @@ -14,8 +14,8 @@ namespace Barotrauma public SoundType Type => Params.State; public Gender Gender => Params.Gender; - public float Volume => roundSound.Volume; - public float Range => roundSound.Range; + public float Volume => roundSound == null ? 0.0f : roundSound.Volume; + public float Range => roundSound == null ? 0.0f : roundSound.Range; public Sound Sound => roundSound?.Sound; public CharacterSound(CharacterParams.SoundParams soundParams) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs index ca43fc95b..4fde706fd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Health/CharacterHealth.cs @@ -246,11 +246,13 @@ namespace Barotrauma } private GUIFrame healthBarHolder; + private Point healthBarOffset { get { - return new Point(5 - (int)Math.Ceiling(1 - 1 * GUI.Scale), (int)Math.Min(Math.Ceiling(17 * GUI.Scale), 20)); + // 0.38775510204f = percentage of offset before reaching the healthbar portion of the graphic going from bottom upwards + return new Point(2, (int)(HUDLayoutSettings.HealthBarArea.Size.Y * 0.38775510204f)); } } @@ -258,7 +260,7 @@ namespace Barotrauma { get { - return new Point(healthBarHolder.Rect.Width - (int)Math.Ceiling(Math.Min(46 * GUI.Scale, 53)), (int)(healthBarHolder.Rect.Height - Math.Min(23 * GUI.Scale, 25)) / 2); + return new Point((int)Math.Ceiling(HUDLayoutSettings.HealthBarArea.Size.X - 45 * GUI.Scale), (int)(healthBarHolder.Rect.Height - Math.Min(23 * GUI.Scale, 25)) / 2); } } @@ -303,7 +305,7 @@ namespace Barotrauma healthShadowSize = 1.0f; healthBar = new GUIProgressBar(new RectTransform(healthBarSize, healthBarHolder.RectTransform, Anchor.BottomRight), - barSize: 1.0f, color: GUIColorSettings.HealthBarColorHigh, style: horizontal ? "CharacterHealthBarSlider" : "GUIProgressBarVertical", showFrame: false) + barSize: 1.0f, color: GUI.Style.HealthBarColorHigh, style: horizontal ? "CharacterHealthBarSlider" : "GUIProgressBarVertical", showFrame: false) { HoverCursor = CursorState.Hand, Enabled = true, @@ -503,8 +505,6 @@ namespace Barotrauma Character.Controlled.AnimController.Anim = (Character.Controlled.AnimController.Anim == AnimController.Animation.CPR) ? AnimController.Animation.None : AnimController.Animation.CPR; - button.Selected = Character.Controlled.AnimController.Anim == AnimController.Animation.CPR; - selectedCharacter.AnimController.ResetPullJoints(); if (GameMain.Client != null) @@ -520,8 +520,10 @@ namespace Barotrauma UpdateAlignment(); - suicideButton = new GUIButton(new RectTransform(new Vector2(0.06f, 0.02f), GUI.Canvas, Anchor.TopCenter) - { MinSize = new Point(150, 20), RelativeOffset = new Vector2(0.0f, 0.01f) }, + suicideButton = new GUIButton(new RectTransform(new Vector2(0.1f, 0.02f), GUI.Canvas, Anchor.TopCenter) + { + MinSize = new Point(150, 20), RelativeOffset = new Vector2(0.0f, 0.01f) + }, TextManager.Get("GiveInButton"), style: "GUIButtonLarge") { ToolTip = TextManager.Get(GameMain.NetworkMember == null ? "GiveInHelpSingleplayer" : "GiveInHelpMultiplayer"), @@ -647,10 +649,14 @@ namespace Barotrauma bloodParticleTimer -= deltaTime * (affliction.Strength / 10.0f); if (bloodParticleTimer <= 0.0f) { + bool inWater = Character.AnimController.InWater; float bloodParticleSize = MathHelper.Lerp(0.5f, 1.0f, affliction.Strength / 100.0f); - if (!Character.AnimController.InWater) bloodParticleSize *= 2.0f; + if (!inWater) + { + bloodParticleSize *= 2.0f; + } var blood = GameMain.ParticleManager.CreateParticle( - Character.AnimController.InWater ? "waterblood" : "blooddrop", + inWater ? Character.Params.BleedParticleWater : Character.Params.BleedParticleAir, targetLimb.WorldPosition, Rand.Vector(affliction.Strength), 0.0f, Character.AnimController.CurrentHull); if (blood != null) @@ -761,7 +767,8 @@ namespace Barotrauma { OpenHealthWindow = null; } - else if (Character.Controlled == Character && Character.Controlled.FocusedCharacter == null) + else if (Character.Controlled == Character && + (Character.Controlled.FocusedCharacter?.CharacterHealth == null || !Character.Controlled.FocusedCharacter.CharacterHealth.UseHealthWindow)) { OpenHealthWindow = this; forceAfflictionContainerUpdate = true; @@ -847,7 +854,7 @@ namespace Barotrauma } else { - healthBar.Color = healthWindowHealthBar.Color = ToolBox.GradientLerp(DisplayedVitality / MaxVitality, GUIColorSettings.HealthBarColorLow, GUIColorSettings.HealthBarColorMedium, GUIColorSettings.HealthBarColorHigh); + healthBar.Color = healthWindowHealthBar.Color = ToolBox.GradientLerp(DisplayedVitality / MaxVitality, GUI.Style.HealthBarColorLow, GUI.Style.HealthBarColorMedium, GUI.Style.HealthBarColorHigh); healthBar.HoverColor = healthWindowHealthBar.HoverColor = healthBar.Color * 2.0f; healthBar.BarSize = healthWindowHealthBar.BarSize = (DisplayedVitality > 0.0f) ? @@ -934,7 +941,7 @@ namespace Barotrauma healthBar.State = GUIComponent.ComponentState.None; } - suicideButton.Visible = Character == Character.Controlled && Character.IsUnconscious && !Character.IsDead; + suicideButton.Visible = Character == Character.Controlled && !Character.IsDead && Character.IsIncapacitated; cprButton.Visible = Character == Character.Controlled?.SelectedCharacter @@ -942,6 +949,10 @@ namespace Barotrauma && !Character.IsDead && openHealthWindow == this; cprButton.IgnoreLayoutGroups = !cprButton.Visible; + cprButton.Selected = + Character.Controlled != null && + Character == Character.Controlled.SelectedCharacter && + Character.Controlled.AnimController.Anim == AnimController.Animation.CPR; cprFrame.RectTransform.Resize(new Vector2(0.7f, 1.0f)); cprButton.RectTransform.Resize(new Vector2(1.0f, 1.0f)); @@ -1132,11 +1143,11 @@ namespace Barotrauma { if (prefab.IsBuff) { - return ToolBox.GradientLerp(affliction.Strength / prefab.MaxStrength, GUIColorSettings.BuffColorLow, GUIColorSettings.BuffColorMedium, GUIColorSettings.BuffColorHigh); + return ToolBox.GradientLerp(affliction.Strength / prefab.MaxStrength, GUI.Style.BuffColorLow, GUI.Style.BuffColorMedium, GUI.Style.BuffColorHigh); } else { - return ToolBox.GradientLerp(affliction.Strength / prefab.MaxStrength, GUIColorSettings.DebuffColorLow, GUIColorSettings.DebuffColorMedium, GUIColorSettings.DebuffColorHigh); + return ToolBox.GradientLerp(affliction.Strength / prefab.MaxStrength, GUI.Style.DebuffColorLow, GUI.Style.DebuffColorMedium, GUI.Style.DebuffColorHigh); } } else @@ -1870,10 +1881,11 @@ namespace Barotrauma healthBarHolder.Visible = value; } + private readonly List> newAfflictions = new List>(); + private readonly List> newLimbAfflictions = new List>(); public void ClientRead(IReadMessage inc) { - List> newAfflictions = new List>(); - + newAfflictions.Clear(); byte afflictionCount = inc.ReadByte(); for (int i = 0; i < afflictionCount; i++) { @@ -1913,7 +1925,7 @@ namespace Barotrauma } } - List> newLimbAfflictions = new List>(); + newLimbAfflictions.Clear(); byte limbAfflictionCount = inc.ReadByte(); for (int i = 0; i < limbAfflictionCount; i++) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs index 0b42757c8..ecdea8d8c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Limb.cs @@ -83,22 +83,22 @@ namespace Barotrauma // pos.Y = -pos.Y; // ShapeExtensions.DrawPoint(spriteBatch, pos, GUI.Style.Red, size: 5); //} - return; + // A debug visualisation on the bezier curve between limbs. - var start = LimbA.WorldPosition; + /*var start = LimbA.WorldPosition; var end = LimbB.WorldPosition; var jointAPos = ConvertUnits.ToDisplayUnits(LocalAnchorA); var control = start + Vector2.Transform(jointAPos, Matrix.CreateRotationZ(LimbA.Rotation)); start.Y = -start.Y; end.Y = -end.Y; control.Y = -control.Y; - //GUI.DrawRectangle(spriteBatch, start, Vector2.One * 5, Color.White, true); - //GUI.DrawRectangle(spriteBatch, end, Vector2.One * 5, Color.Black, true); - //GUI.DrawRectangle(spriteBatch, control, Vector2.One * 5, Color.Black, true); - //GUI.DrawLine(spriteBatch, start, end, Color.White); - //GUI.DrawLine(spriteBatch, start, control, Color.Black); - //GUI.DrawLine(spriteBatch, control, end, Color.Black); - GUI.DrawBezierWithDots(spriteBatch, start, end, control, 1000, GUI.Style.Red); + GUI.DrawRectangle(spriteBatch, start, Vector2.One * 5, Color.White, true); + GUI.DrawRectangle(spriteBatch, end, Vector2.One * 5, Color.Black, true); + GUI.DrawRectangle(spriteBatch, control, Vector2.One * 5, Color.Black, true); + GUI.DrawLine(spriteBatch, start, end, Color.White); + GUI.DrawLine(spriteBatch, start, control, Color.Black); + GUI.DrawLine(spriteBatch, control, end, Color.Black); + GUI.DrawBezierWithDots(spriteBatch, start, end, control, 1000, GUI.Style.Red);*/ } } @@ -110,6 +110,7 @@ namespace Barotrauma private float wetTimer; private float dripParticleTimer; + private float deadTimer; /// /// Note that different limbs can share the same deformations. @@ -418,14 +419,23 @@ namespace Barotrauma } } - partial void AddDamageProjSpecific(Vector2 simPosition, List afflictions, bool playSound, List appliedDamageModifiers) + partial void AddDamageProjSpecific(IEnumerable afflictions, bool playSound, IEnumerable appliedDamageModifiers) { - float bleedingDamage = character.CharacterHealth.DoesBleed ? afflictions.FindAll(a => a is AfflictionBleeding).Sum(a => a.GetVitalityDecrease(character.CharacterHealth)) : 0; - float damage = afflictions.FindAll(a => a.Prefab.AfflictionType == "damage").Sum(a => a.GetVitalityDecrease(character.CharacterHealth)); + float bleedingDamage = character.CharacterHealth.DoesBleed ? afflictions.Where(a => a is AfflictionBleeding).Sum(a => a.GetVitalityDecrease(character.CharacterHealth)) : 0; + float damage = afflictions.Where(a => a.Prefab.AfflictionType == "damage").Sum(a => a.GetVitalityDecrease(character.CharacterHealth)); float damageMultiplier = 1; foreach (DamageModifier damageModifier in appliedDamageModifiers) { - damageMultiplier *= damageModifier.DamageMultiplier; + foreach (var afflictionPrefab in AfflictionPrefab.List) + { + if (damageModifier.MatchesAffliction(afflictionPrefab.Identifier, afflictionPrefab.AfflictionType)) + { + if (afflictionPrefab.Effects.Any(e => e.MaxVitalityDecrease > 0)) + { + damageMultiplier *= damageModifier.DamageMultiplier; + } + } + } } if (playSound) { @@ -477,13 +487,21 @@ namespace Barotrauma partial void UpdateProjSpecific(float deltaTime) { - if (!body.Enabled) return; + if (!body.Enabled) { return; } if (!character.IsDead) { DamageOverlayStrength -= deltaTime; BurnOverlayStrength -= deltaTime; } + else + { + var spriteParams = Params.GetSprite(); + if (spriteParams.DeadColorTime > 0 && deadTimer < spriteParams.DeadColorTime) + { + deadTimer += deltaTime; + } + } if (inWater) { @@ -524,7 +542,12 @@ namespace Barotrauma public void Draw(SpriteBatch spriteBatch, Camera cam, Color? overrideColor = null) { float brightness = 1.0f - (burnOverLayStrength / 100.0f) * 0.5f; - Color color = new Color(brightness, brightness, brightness); + var spriteParams = Params.GetSprite(); + Color color = new Color(spriteParams.Color.R / 255f * brightness, spriteParams.Color.G / 255f * brightness, spriteParams.Color.B / 255f * brightness, spriteParams.Color.A / 255f); + if (deadTimer > 0) + { + color = Color.Lerp(color, spriteParams.DeadColor, MathUtils.InverseLerp(0, spriteParams.DeadColorTime, deadTimer)); + } color = overrideColor ?? color; @@ -594,13 +617,19 @@ namespace Barotrauma foreach (var decorativeSprite in DecorativeSprites) { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } + Color c = new Color(decorativeSprite.Color.R / 255f * brightness, decorativeSprite.Color.G / 255f * brightness, decorativeSprite.Color.B / 255f * brightness, decorativeSprite.Color.A / 255f); + if (deadTimer > 0) + { + c = Color.Lerp(c, spriteParams.DeadColor, MathUtils.InverseLerp(0, Params.GetSprite().DeadColorTime, deadTimer)); + } + c = overrideColor ?? c; float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState); Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState) * Scale; var ca = (float)Math.Cos(-body.Rotation); var sa = (float)Math.Sin(-body.Rotation); Vector2 transformedOffset = new Vector2(ca * offset.X + sa * offset.Y, -sa * offset.X + ca * offset.Y); - decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(body.DrawPosition.X + transformedOffset.X, -(body.DrawPosition.Y + transformedOffset.Y)), color, - -body.Rotation + rotation, Scale, spriteEffect, + decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(body.DrawPosition.X + transformedOffset.X, -(body.DrawPosition.Y + transformedOffset.Y)), c, + -body.Rotation + rotation, decorativeSprite.Scale * Scale, spriteEffect, depth: decorativeSprite.Sprite.Depth); } float depthStep = 0.000001f; diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index 8728060a4..ea20b2c74 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -12,6 +12,7 @@ using System.Xml.Linq; using System.Globalization; using FarseerPhysics; using Barotrauma.Extensions; +using Barotrauma.Steam; namespace Barotrauma { @@ -217,6 +218,8 @@ namespace Barotrauma case "toggleupperhud": case "togglecharacternames": case "fpscounter": + case "dumptofile": + case "findentityids": return true; default: return client.HasConsoleCommandPermission(command); @@ -423,7 +426,8 @@ namespace Barotrauma { if (args.Length > 0) { - Submarine.Load(string.Join(" ", args), true); + var subInfo = new SubmarineInfo(string.Join(" ", args)); + Submarine.MainSub = Submarine.Load(subInfo, true); } GameMain.SubEditorScreen.Select(); }, isCheat: true)); @@ -464,16 +468,23 @@ namespace Barotrauma } }, isCheat: true)); + commands.Add(new Command("steamnetdebug", "steamnetdebug: Toggles Steamworks debug logging.", (string[] args) => + { + SteamManager.NetworkingDebugLog = !SteamManager.NetworkingDebugLog; + })); + AssignRelayToServer("kick", false); AssignRelayToServer("kickid", false); AssignRelayToServer("ban", false); AssignRelayToServer("banid", false); AssignRelayToServer("dumpids", false); + AssignRelayToServer("dumptofile", false); AssignRelayToServer("findentityids", false); AssignRelayToServer("campaigninfo", false); AssignRelayToServer("help", false); AssignRelayToServer("verboselogging", false); AssignRelayToServer("freecam", false); + AssignRelayToServer("steamnetdebug", false); #if DEBUG AssignRelayToServer("crash", false); AssignRelayToServer("simulatedlatency", false); @@ -513,13 +524,14 @@ namespace Barotrauma AssignOnExecute("explosion", (string[] args) => { Vector2 explosionPos = GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition); - float range = 500, force = 10, damage = 50, structureDamage = 10, empStrength = 0.0f; + float range = 500, force = 10, damage = 50, structureDamage = 10, itemDamage = 100, empStrength = 0.0f; if (args.Length > 0) float.TryParse(args[0], out range); if (args.Length > 1) float.TryParse(args[1], out force); if (args.Length > 2) float.TryParse(args[2], out damage); if (args.Length > 3) float.TryParse(args[3], out structureDamage); - if (args.Length > 4) float.TryParse(args[4], out empStrength); - new Explosion(range, force, damage, structureDamage, empStrength).Explode(explosionPos, null); + if (args.Length > 4) float.TryParse(args[4], out itemDamage); + if (args.Length > 5) float.TryParse(args[5], out empStrength); + new Explosion(range, force, damage, structureDamage, itemDamage, empStrength).Explode(explosionPos, null); }); AssignOnExecute("teleportcharacter|teleport", (string[] args) => @@ -834,7 +846,7 @@ namespace Barotrauma return; } - if (Submarine.SaveCurrent(System.IO.Path.Combine(Submarine.SavePath, fileName + ".sub"))) + if (Submarine.MainSub.SaveAs(System.IO.Path.Combine(SubmarineInfo.SavePath, fileName + ".sub"))) { NewMessage("Sub saved", Color.Green); } @@ -843,7 +855,8 @@ namespace Barotrauma commands.Add(new Command("load|loadsub", "load [submarine name]: Load a submarine.", (string[] args) => { if (args.Length == 0) return; - Submarine.Load(string.Join(" ", args), true); + SubmarineInfo subInfo = new SubmarineInfo(string.Join(" ", args)); + Submarine.Load(subInfo, true); })); commands.Add(new Command("cleansub", "", (string[] args) => @@ -1086,23 +1099,69 @@ namespace Barotrauma if (args.Length != 2 || Screen.Selected != GameMain.SubEditorScreen) { return; } foreach (MapEntity me in MapEntity.SelectedList) { - if (me is ISerializableEntity serializableEntity) + bool propertyFound = false; + if (!(me is ISerializableEntity serializableEntity)) { continue; } + if (serializableEntity.SerializableProperties == null) { continue; } + + if (serializableEntity.SerializableProperties.TryGetValue(args[0].ToLowerInvariant(), out SerializableProperty property)) { - if (serializableEntity.SerializableProperties == null) + propertyFound = true; + object prevValue = property.GetValue(me); + if (property.TrySetValue(me, args[1])) { - continue; + NewMessage($"Changed the value \"{args[0]}\" from {(prevValue?.ToString() ?? null)} to {args[1]} on entity \"{me.ToString()}\".", Color.LightGreen); } - if (!serializableEntity.SerializableProperties.TryGetValue(args[0].ToLowerInvariant(), out SerializableProperty property)) + else { - NewMessage("Property \"" + args[0] + "\" not found in the entity \"" + me.ToString() + "\".", Color.Orange); - continue; + NewMessage($"Failed to set the value of \"{args[0]}\" to \"{args[1]}\" on the entity \"{me.ToString()}\".", Color.Orange); } - if (!property.TrySetValue(me, args[1])) + } + if (me is Item item) + { + foreach (ItemComponent ic in item.Components) { - NewMessage("Failed to set the value of \"" + args[0] + "\" to \"" + args[1] + "\" on the entity \"" + me.ToString() + "\".", Color.Orange); + ic.SerializableProperties.TryGetValue(args[0].ToLowerInvariant(), out SerializableProperty componentProperty); + if (componentProperty == null) { continue; } + propertyFound = true; + object prevValue = componentProperty.GetValue(ic); + if (componentProperty.TrySetValue(ic, args[1])) + { + NewMessage($"Changed the value \"{args[0]}\" from {prevValue} to {args[1]} on item \"{me.ToString()}\", component \"{ic.GetType().Name}\".", Color.LightGreen); + } + else + { + NewMessage($"Failed to set the value of \"{args[0]}\" to \"{args[1]}\" on the item \"{me.ToString()}\", component \"{ic.GetType().Name}\".", Color.Orange); + } + } + } + if (!propertyFound) + { + NewMessage($"Property \"{args[0]}\" not found in the entity \"{me.ToString()}\".", Color.Orange); + } + } + }, + () => + { + List propertyList = new List(); + foreach (MapEntity me in MapEntity.SelectedList) + { + if (!(me is ISerializableEntity serializableEntity)) { continue; } + if (serializableEntity.SerializableProperties == null) { continue; } + propertyList.AddRange(serializableEntity.SerializableProperties.Select(p => p.Key)); + if (me is Item item) + { + foreach (ItemComponent ic in item.Components) + { + propertyList.AddRange(ic.SerializableProperties.Select(p => p.Key)); } } } + + return new string[][] + { + propertyList.Distinct().ToArray(), + new string[0] + }; })); commands.Add(new Command("checkmissingloca", "", (string[] args) => @@ -1135,7 +1194,7 @@ namespace Barotrauma } } - foreach (Submarine sub in Submarine.SavedSubmarines) + foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) { string nameIdentifier = "submarine.name." + sub.Name.ToLowerInvariant(); if (!tags[language].Contains(nameIdentifier)) @@ -2269,7 +2328,8 @@ namespace Barotrauma } try { - Submarine spawnedSub = Submarine.Load(args[0], false); + SubmarineInfo subInfo = new SubmarineInfo(args[0]); + Submarine spawnedSub = Submarine.Load(subInfo, false); spawnedSub.SetPosition(GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition)); } catch (Exception e) @@ -2347,7 +2407,7 @@ namespace Barotrauma switch (firstArg) { case "name": - var sprites = Sprite.LoadedSprites.Where(s => s.Name?.ToLowerInvariant() == secondArg.ToLowerInvariant()); + var sprites = Sprite.LoadedSprites.Where(s => s.Name != null && s.Name.Equals(secondArg, StringComparison.OrdinalIgnoreCase)); if (sprites.Any()) { foreach (var s in sprites) @@ -2363,7 +2423,7 @@ namespace Barotrauma } case "identifier": case "id": - sprites = Sprite.LoadedSprites.Where(s => s.EntityID?.ToLowerInvariant() == secondArg.ToLowerInvariant()); + sprites = Sprite.LoadedSprites.Where(s => s.EntityID != null && s.EntityID.Equals(secondArg, StringComparison.OrdinalIgnoreCase)); if (sprites.Any()) { foreach (var s in sprites) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs index 58d480b8a..878c814b7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/MissionPrefab.cs @@ -24,7 +24,7 @@ namespace Barotrauma { foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "icon") { continue; } + if (!subElement.Name.ToString().Equals("icon", StringComparison.OrdinalIgnoreCase)) { continue; } Icon = new Sprite(subElement); IconColor = subElement.GetAttributeColor("color", Color.White); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs index b455fea48..8755b9d40 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Events/Missions/SalvageMission.cs @@ -7,13 +7,38 @@ namespace Barotrauma { public override void ClientReadInitial(IReadMessage msg) { - item = Item.ReadSpawnData(msg); - if (item == null) + bool usedExistingItem = msg.ReadBoolean(); + if (usedExistingItem) { - throw new System.Exception("Error in SalvageMission.ClientReadInitial: spawned item was null (mission: " + Prefab.Identifier + ")"); + ushort id = msg.ReadUInt16(); + item = Entity.FindEntityByID(id) as Item; + if (item == null) + { + throw new System.Exception("Error in SalvageMission.ClientReadInitial: failed to find item " + id + " (mission: " + Prefab.Identifier + ")"); + } + } + else + { + item = Item.ReadSpawnData(msg); + if (item == null) + { + throw new System.Exception("Error in SalvageMission.ClientReadInitial: spawned item was null (mission: " + Prefab.Identifier + ")"); + } } - item.body.FarseerBody.BodyType = BodyType.Kinematic; + int executedEffectCount = msg.ReadByte(); + for (int i = 0; i < executedEffectCount; i++) + { + int index1 = msg.ReadByte(); + int index2 = msg.ReadByte(); + var selectedEffect = statusEffects[index1][index2]; + item.ApplyStatusEffect(selectedEffect, selectedEffect.type, deltaTime: 1.0f, worldPosition: item.Position); + } + + if (item.body != null) + { + item.body.FarseerBody.BodyType = BodyType.Kinematic; + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs index 74a57080f..c16ab84b2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Fonts/ScalableFont.cs @@ -7,76 +7,6 @@ using System.Xml.Linq; namespace Barotrauma { - public class ColorData - { - public int StartIndex, EndIndex; - public Color Color; - - private const char colorDefinitionIndicator = '‖'; - private const char lineChangeIndicator = '\n'; - private const string colorDefinitionStartString = "‖color:"; - private const string coloringEndDefinition = "‖color:end‖"; - - public static List GetColorData(string text, out string sanitizedText) - { - List textColors = null; - if (text != null && text.IndexOf(colorDefinitionIndicator) != -1 && text.Contains(colorDefinitionStartString)) - { - textColors = new List(); - List lineChangeIndexes = null; - - int currentIndex = text.IndexOf(lineChangeIndicator); - if (currentIndex != -1) - { - lineChangeIndexes = new List(); - lineChangeIndexes.Add(currentIndex); - int startIndex = currentIndex + 1; - - while (true) - { - if (startIndex >= text.Length) break; - currentIndex = text.IndexOf(lineChangeIndicator, startIndex); - if (currentIndex == -1) break; - lineChangeIndexes.Add(currentIndex); - startIndex = currentIndex + 1; - } - } - - while (text.IndexOf(colorDefinitionStartString) != -1) - { - ColorData colorData = new ColorData(); - - int colorDefinitionStartIndex = text.IndexOf(colorDefinitionStartString); - int colorDefinitionEndIndex = text.IndexOf(colorDefinitionIndicator, colorDefinitionStartIndex + 1); - - string[] colorDefinition = text.Substring(colorDefinitionStartIndex + colorDefinitionStartString.Length, colorDefinitionEndIndex - colorDefinitionStartIndex - colorDefinitionStartString.Length).Split(','); - - colorData.StartIndex = colorDefinitionStartIndex; - colorData.Color = new Color(int.Parse(colorDefinition[0]), int.Parse(colorDefinition[1]), int.Parse(colorDefinition[2])); - text = text.Remove(colorDefinitionStartIndex, colorDefinitionEndIndex - colorDefinitionStartIndex + 1); - colorData.EndIndex = text.IndexOf(coloringEndDefinition); - text = text.Remove(colorData.EndIndex, coloringEndDefinition.Length); - - if (lineChangeIndexes != null) - { - for (int i = 0; i < lineChangeIndexes.Count; i++) - { - if (colorData.StartIndex > lineChangeIndexes[i]) - { - colorData.StartIndex--; - colorData.EndIndex--; - } - } - } - - textColors.Add(colorData); - } - } - - sanitizedText = text; - return textColors; - } - } public class ScalableFont : IDisposable { private static List FontList = new List(); @@ -492,12 +422,12 @@ namespace Barotrauma } } - public void DrawStringWithColors(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects se, float layerDepth, List colorData) + public void DrawStringWithColors(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects se, float layerDepth, List richTextData) { - DrawStringWithColors(sb, text, position, color, rotation, origin, new Vector2(scale), se, layerDepth, colorData); + DrawStringWithColors(sb, text, position, color, rotation, origin, new Vector2(scale), se, layerDepth, richTextData); } - public void DrawStringWithColors(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth, List colorData) + public void DrawStringWithColors(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth, List richTextData) { if (textures.Count == 0 && !DynamicLoading) { return; } @@ -505,8 +435,8 @@ namespace Barotrauma Vector2 currentPos = position; Vector2 advanceUnit = rotation == 0.0f ? Vector2.UnitX : new Vector2((float)Math.Cos(rotation), (float)Math.Sin(rotation)); - int colorDataIndex = 0; - ColorData currentColorData = colorData[colorDataIndex]; + int richTextDataIndex = 0; + RichTextData currentRichTextData = richTextData[richTextDataIndex]; for (int i = 0; i < text.Length; i++) { @@ -527,15 +457,19 @@ namespace Barotrauma Color currentTextColor; - if (currentColorData != null && i > currentColorData.EndIndex + lineNum) + if (currentRichTextData != null && i > currentRichTextData.EndIndex + lineNum) { - colorDataIndex++; - currentColorData = colorDataIndex < colorData.Count ? colorData[colorDataIndex] : null; + richTextDataIndex++; + currentRichTextData = richTextDataIndex < richTextData.Count ? richTextData[richTextDataIndex] : null; } - if (currentColorData != null && currentColorData.StartIndex + lineNum <= i && i <= currentColorData.EndIndex + lineNum) + if (currentRichTextData != null && currentRichTextData.StartIndex + lineNum <= i && i <= currentRichTextData.EndIndex + lineNum) { - currentTextColor = currentColorData.Color; + currentTextColor = currentRichTextData.Color ?? color; + if (!string.IsNullOrEmpty(currentRichTextData.Metadata)) + { + currentTextColor = Color.Lerp(currentTextColor, Color.White, 0.5f); + } } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs index 39494a661..67c14dc5f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs @@ -52,7 +52,20 @@ namespace Barotrauma public GUITextBox InputBox { get; private set; } - public GUIButton ToggleButton; + private GUIButton toggleButton; + + public GUIButton ToggleButton + { + get => toggleButton; + set + { + if (toggleButton != null) + { + toggleButton.RectTransform.Parent = null; + } + toggleButton = value; + } + } private GUIButton showNewMessagesButton; @@ -190,22 +203,49 @@ namespace Barotrauma var msgHolder = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.0f), chatBox.Content.RectTransform, Anchor.TopCenter), style: null, color: ((chatBox.Content.CountChildren % 2) == 0) ? Color.Transparent : Color.Black * 0.1f); - GUITextBlock senderNameBlock = new GUITextBlock(new RectTransform(new Vector2(0.98f, 0.0f), msgHolder.RectTransform) { AbsoluteOffset = new Point((int)(5 * GUI.Scale), 0) }, + GUITextBlock senderNameTimestamp = new GUITextBlock(new RectTransform(new Vector2(0.98f, 0.0f), msgHolder.RectTransform) { AbsoluteOffset = new Point((int)(5 * GUI.Scale), 0) }, ChatMessage.GetTimeStamp(), textColor: Color.LightGray, font: GUI.SmallFont, textAlignment: Alignment.TopLeft, style: null) { CanBeFocused = true }; if (!string.IsNullOrEmpty(senderName)) { - new GUITextBlock(new RectTransform(new Vector2(0.8f, 1.0f), senderNameBlock.RectTransform) { AbsoluteOffset = new Point((int)(senderNameBlock.TextSize.X), 0) }, - senderName, textColor: senderColor, font: GUI.SmallFont, textAlignment: Alignment.TopLeft, style: null) + var senderNameBlock = new GUIButton(new RectTransform(new Vector2(0.8f, 1.0f), senderNameTimestamp.RectTransform) { AbsoluteOffset = new Point((int)(senderNameTimestamp.TextSize.X), 0) }, + senderName, textAlignment: Alignment.TopLeft, style: null, color: Color.Transparent) { - CanBeFocused = true + TextBlock = + { + Padding = Vector4.Zero + }, + Font = GUI.SmallFont, + CanBeFocused = true, + ForceUpperCase = false, + UserData = message.SenderClient, + OnClicked = (_, o) => + { + if (!(o is Client client)) { return false; } + GameMain.NetLobbyScreen?.SelectPlayer(client); + return true; + }, + OnSecondaryClicked = (_, o) => + { + if (!(o is Client client)) { return false; } + GameMain.GameSession?.CrewManager?.CreateModerationContextMenu(PlayerInput.MousePosition.ToPoint(), client); + return true; + }, + Text = senderName }; + + senderNameBlock.RectTransform.NonScaledSize = senderNameBlock.TextBlock.TextSize.ToPoint(); + senderNameBlock.TextBlock.OverrideTextColor(senderColor); + if (senderNameBlock.UserData != null) + { + senderNameBlock.TextBlock.HoverTextColor = Color.White; + } } var msgText =new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), msgHolder.RectTransform) - { AbsoluteOffset = new Point((int)(10 * GUI.Scale), senderNameBlock == null ? 0 : senderNameBlock.Rect.Height) }, + { AbsoluteOffset = new Point((int)(10 * GUI.Scale), senderNameTimestamp == null ? 0 : senderNameTimestamp.Rect.Height) }, displayedText, textColor: message.Color, font: GUI.SmallFont, textAlignment: Alignment.TopLeft, style: null, wrap: true, color: ((chatBox.Content.CountChildren % 2) == 0) ? Color.Transparent : Color.Black * 0.1f) { @@ -230,7 +270,7 @@ namespace Barotrauma msgHolder.RectTransform.SizeChanged -= Recalculate; //resize the holder to match the size of the message and add some spacing msgText.RectTransform.MaxSize = new Point(msgHolder.Rect.Width - msgText.RectTransform.AbsoluteOffset.X, int.MaxValue); - senderNameBlock.RectTransform.MaxSize = new Point(msgHolder.Rect.Width - senderNameBlock.RectTransform.AbsoluteOffset.X, int.MaxValue); + senderNameTimestamp.RectTransform.MaxSize = new Point(msgHolder.Rect.Width - senderNameTimestamp.RectTransform.AbsoluteOffset.X, int.MaxValue); msgHolder.Children.ForEach(c => (c as GUITextBlock)?.CalculateHeightFromText()); msgHolder.RectTransform.Resize(new Point(msgHolder.Rect.Width, msgHolder.Children.Sum(c => c.Rect.Height) + (int)(10 * GUI.Scale)), resizeChildren: false); msgHolder.RectTransform.SizeChanged += Recalculate; @@ -247,6 +287,11 @@ namespace Barotrauma showNewMessagesButton.Visible = true; } + if (message.Type == ChatMessageType.Server && message.ChangeType != PlayerConnectionChangeType.None) + { + TabMenu.StorePlayerConnectionChangeMessage(message); + } + if (!ToggleOpen) { var popupMsg = new GUIFrame(new RectTransform(Vector2.One, GUIFrame.RectTransform), style: "GUIToolTip") diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs index f9d0b8092..9069091ef 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ComponentStyle.cs @@ -149,7 +149,7 @@ namespace Barotrauma Point size = new Point(0, 0); foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "size") { continue; } + if (!subElement.Name.ToString().Equals("size", StringComparison.OrdinalIgnoreCase)) { continue; } Point maxResolution = subElement.GetAttributePoint("maxresolution", new Point(int.MaxValue, int.MaxValue)); if (GameMain.GraphicsWidth <= maxResolution.X && GameMain.GraphicsHeight <= maxResolution.Y) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs index b813565e0..167e80dc1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs @@ -348,17 +348,24 @@ namespace Barotrauma } IEnumerable files = null; - foreach (string pattern in currentFileTypePattern.Split(',')) + if (currentFileTypePattern == null) { - string patternTrimmed = pattern.Trim(); - patternTrimmed = "*" + filterBox.Text + "*" + patternTrimmed; - if (files == null) + files = Directory.GetFiles(currentDirectory); + } + else + { + foreach (string pattern in currentFileTypePattern.Split(',')) { - files = Directory.EnumerateFiles(currentDirectory, patternTrimmed); - } - else - { - files = files.Concat(Directory.EnumerateFiles(currentDirectory, patternTrimmed)); + string patternTrimmed = pattern.Trim(); + patternTrimmed = "*" + filterBox.Text + "*" + patternTrimmed; + if (files == null) + { + files = Directory.EnumerateFiles(currentDirectory, patternTrimmed); + } + else + { + files = files.Concat(Directory.EnumerateFiles(currentDirectory, patternTrimmed)); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index e03665277..17d668614 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -26,7 +26,8 @@ namespace Barotrauma Click, PickItem, PickItemFail, - DropItem + DropItem, + PopupMenu } public enum CursorState @@ -82,6 +83,9 @@ namespace Barotrauma public static float Scale => (GameMain.GraphicsWidth / ReferenceResolution.X + GameMain.GraphicsHeight / ReferenceResolution.Y) / 2.0f * GameSettings.HUDScale; public static float xScale => GameMain.GraphicsWidth / ReferenceResolution.X * GameSettings.HUDScale; public static float yScale => GameMain.GraphicsHeight / ReferenceResolution.Y * GameSettings.HUDScale; + public static int IntScale(float f) => (int)(f * Scale); + public static int IntScaleFloor(float f) => (int)Math.Floor(f * Scale); + public static int IntScaleCeiling(float f) => (int) Math.Ceiling(f * Scale); public static float HorizontalAspectRatio => GameMain.GraphicsWidth / (float)GameMain.GraphicsHeight; public static float VerticalAspectRatio => GameMain.GraphicsHeight / (float)GameMain.GraphicsWidth; public static float RelativeHorizontalAspectRatio => HorizontalAspectRatio / (ReferenceResolution.X / ReferenceResolution.Y); @@ -242,6 +246,7 @@ namespace Barotrauma sounds[(int)GUISoundType.RadioMessage] = GameMain.SoundManager.LoadSound("Content/Sounds/UI/RadioMsg.ogg", false); sounds[(int)GUISoundType.DeadMessage] = GameMain.SoundManager.LoadSound("Content/Sounds/UI/DeadMsg.ogg", false); sounds[(int)GUISoundType.Click] = GameMain.SoundManager.LoadSound("Content/Sounds/UI/Click.ogg", false); + sounds[(int)GUISoundType.PopupMenu] = GameMain.SoundManager.LoadSound("Content/Sounds/UI/PopupMenu.ogg", false); sounds[(int)GUISoundType.PickItem] = GameMain.SoundManager.LoadSound("Content/Sounds/PickItem.ogg", false); sounds[(int)GUISoundType.PickItemFail] = GameMain.SoundManager.LoadSound("Content/Sounds/PickItemFail.ogg", false); @@ -494,9 +499,27 @@ namespace Barotrauma if (MouseOn != null) { - DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - 500, 20), - $"Selected UI Element: {MouseOn.GetType().Name} ({ (MouseOn.Style?.Element.Name.LocalName ?? "no style") }, {MouseOn.Rect})", - Color.LightGreen, Color.Black * 0.5f, 0, SmallFont); + RectTransform mouseOnRect = MouseOn.RectTransform; + bool isAbsoluteOffsetInUse = mouseOnRect.AbsoluteOffset != Point.Zero || mouseOnRect.RelativeOffset == Vector2.Zero; + + string selectedString = $"Selected UI Element: {MouseOn.GetType().Name} ({ MouseOn.Style?.Element.Name.LocalName ?? "no style" }, {MouseOn.Rect}"; + string offsetString = $"Relative Offset: {mouseOnRect.RelativeOffset} | Absolute Offset: {(isAbsoluteOffsetInUse ? mouseOnRect.AbsoluteOffset : mouseOnRect.ParentRect.MultiplySize(mouseOnRect.RelativeOffset))}{(isAbsoluteOffsetInUse ? "" : " (Calculated from RelativeOffset)")}"; + string anchorPivotString = $"Anchor: {mouseOnRect.Anchor} | Pivot: {mouseOnRect.Pivot}"; + Vector2 selectedStringSize = SmallFont.MeasureString(selectedString); + Vector2 offsetStringSize = SmallFont.MeasureString(offsetString); + Vector2 anchorPivotStringSize = SmallFont.MeasureString(anchorPivotString); + + int padding = IntScale(10); + int yPos = padding; + + DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)selectedStringSize.X - padding, yPos), selectedString, Color.LightGreen, Color.Black, 0, SmallFont); + yPos += (int)selectedStringSize.Y + padding / 2; + + DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)offsetStringSize.X - padding, yPos), offsetString, Color.LightGreen, Color.Black, 0, SmallFont); + yPos += (int)offsetStringSize.Y + padding / 2; + + DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)anchorPivotStringSize.X - padding, yPos), anchorPivotString, Color.LightGreen, Color.Black, 0, SmallFont); + yPos += (int)anchorPivotStringSize.Y + padding / 2; } } @@ -519,6 +542,34 @@ namespace Barotrauma MouseOn.DrawToolTip(spriteBatch); } + if (SubEditorScreen.IsSubEditor()) + { + // Draw our "infinite stack" on the cursor + switch (SubEditorScreen.DraggedItemPrefab) + { + case ItemPrefab itemPrefab: + { + var sprite = itemPrefab.InventoryIcon ?? itemPrefab.sprite; + sprite?.Draw(spriteBatch, PlayerInput.MousePosition, scale: Math.Min(64 / sprite.size.X, 64 / sprite.size.Y) * Scale); + break; + } + case ItemAssemblyPrefab iPrefab: + { + var (x, y) = PlayerInput.MousePosition; + foreach (var pair in iPrefab.DisplayEntities) + { + Rectangle dRect = pair.Second; + dRect = new Rectangle(x: (int)(dRect.X * iPrefab.Scale + x), + y: (int)(dRect.Y * iPrefab.Scale - y), + width: (int)(dRect.Width * iPrefab.Scale), + height: (int)(dRect.Height * iPrefab.Scale)); + pair.First.DrawPlacing(spriteBatch, dRect, pair.First.Scale * iPrefab.Scale); + } + break; + } + } + } + if (GameMain.WindowActive && !HideCursor) { spriteBatch.End(); @@ -539,6 +590,10 @@ namespace Barotrauma GameMain.GameScreen.PostProcessEffect.Parameters["blurDistance"].SetValue(0.001f * aberrationStrength); GameMain.GameScreen.PostProcessEffect.Parameters["chromaticAberrationStrength"].SetValue(new Vector3(-0.025f, -0.01f, -0.05f) * (float)(PerlinNoise.CalculatePerlin(aberrationT, aberrationT, 0) + 0.5f) * aberrationStrength); + + Matrix.CreateOrthographicOffCenter(0, GameMain.GraphicsWidth, GameMain.GraphicsHeight, 0, 0, -1, out Matrix projection); + + GameMain.GameScreen.PostProcessEffect.Parameters["MatrixTransform"].SetValue(projection); GameMain.GameScreen.PostProcessEffect.CurrentTechnique = GameMain.GameScreen.PostProcessEffect.Techniques["BlurChromaticAberration"]; GameMain.GameScreen.PostProcessEffect.CurrentTechnique.Passes[0].Apply(); @@ -778,6 +833,8 @@ namespace Barotrauma if (MouseCursor == CursorState.Waiting) { return CursorState.Waiting; } if (GUIScrollBar.DraggingBar != null) { return GUIScrollBar.DraggingBar.Bar.HoverCursor; } + if (SubEditorScreen.IsSubEditor() && SubEditorScreen.DraggedItemPrefab != null) { return CursorState.Hand; } + // Wire cursors if (Character.Controlled != null) { @@ -814,8 +871,7 @@ namespace Barotrauma case SubEditorScreen editor: { // Portrait area - if ((editor.CharacterMode || editor.WiringMode) && - HUDLayoutSettings.BottomRightInfoArea.Contains(PlayerInput.MousePosition)) + if (editor.WiringMode && HUDLayoutSettings.BottomRightInfoArea.Contains(PlayerInput.MousePosition)) { return CursorState.Hand; } @@ -1135,7 +1191,7 @@ namespace Barotrauma font.DrawString(sb, text, pos, color); } - public static void DrawStringWithColors(SpriteBatch sb, Vector2 pos, string text, Color color, List colorData, Color? backgroundColor = null, int backgroundPadding = 0, ScalableFont font = null, float depth = 0.0f) + public static void DrawStringWithColors(SpriteBatch sb, Vector2 pos, string text, Color color, List richTextData, Color? backgroundColor = null, int backgroundPadding = 0, ScalableFont font = null, float depth = 0.0f) { if (font == null) font = Font; if (backgroundColor != null) @@ -1144,7 +1200,7 @@ namespace Barotrauma DrawRectangle(sb, pos - Vector2.One * backgroundPadding, textSize + Vector2.One * 2.0f * backgroundPadding, (Color)backgroundColor, true, depth, 5); } - font.DrawStringWithColors(sb, text, pos, color, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, depth, colorData); + font.DrawStringWithColors(sb, text, pos, color, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, depth, richTextData); } public static void DrawRectangle(SpriteBatch sb, Vector2 start, Vector2 size, Color clr, bool isFilled = false, float depth = 0.0f, int thickness = 1) @@ -1922,6 +1978,19 @@ namespace Barotrauma return true; }; } + else if (GameMain.GameSession.GameMode is SubTestMode) + { + button = new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonContainer.RectTransform), text: TextManager.Get("PauseMenuReturnToEditor")) + { + OnClicked = (btn, userdata) => + { + GameMain.GameSession.GameMode.End(""); + + return true; + } + }; + button.OnClicked += TogglePauseMenu; + } else if (!GameMain.GameSession.GameMode.IsSinglePlayer && GameMain.Client != null && GameMain.Client.HasPermission(ClientPermissions.ManageRound)) { new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), buttonContainer.RectTransform), text: TextManager.Get("EndRound")) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIColorSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIColorSettings.cs deleted file mode 100644 index 7b402000f..000000000 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIColorSettings.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Microsoft.Xna.Framework; - -namespace Barotrauma -{ - public class GUIColorSettings - { - // Inventory - public static Color EquipmentSlotIconColor = new Color(99, 70, 64); - - // Health HUD - public static Color BuffColorLow = Color.LightGreen; - public static Color BuffColorMedium = Color.Green; - public static Color BuffColorHigh = Color.DarkGreen; - - public static Color DebuffColorLow = Color.DarkSalmon; - public static Color DebuffColorMedium = Color.Red; - public static Color DebuffColorHigh = Color.DarkRed; - - public static Color HealthBarColorLow = Color.Red; - public static Color HealthBarColorMedium = Color.Orange; - public static Color HealthBarColorHigh = new Color(78, 114, 88); - - } -} diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs index 077ac5ec4..b8e4012a1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIComponent.cs @@ -18,6 +18,9 @@ namespace Barotrauma public CursorState HoverCursor = CursorState.Default; + public delegate bool SecondaryButtonDownHandler(GUIComponent component, object userData); + public SecondaryButtonDownHandler OnSecondaryClicked; + public IEnumerable Children => RectTransform.Children.Select(c => c.GUIComponent); public T GetChild() where T : GUIComponent @@ -202,12 +205,12 @@ namespace Barotrauma set { RawToolTip = value; - TooltipColorData = ColorData.GetColorData(value, out value); + TooltipRichTextData = RichTextData.GetRichTextData(value, out value); toolTip = value; } } - public List TooltipColorData = null; + public List TooltipRichTextData = null; public GUIComponentStyle Style { @@ -451,6 +454,15 @@ namespace Barotrauma protected virtual void Update(float deltaTime) { if (!Visible) return; + + if (CanBeFocused && OnSecondaryClicked != null) + { + if (GUI.IsMouseOn(this) && PlayerInput.SecondaryMouseButtonClicked()) + { + OnSecondaryClicked?.Invoke(this, userData); + } + } + if (flashTimer > 0.0f) { flashTimer -= deltaTime; @@ -638,10 +650,10 @@ namespace Barotrauma public void DrawToolTip(SpriteBatch spriteBatch) { if (!Visible) return; - DrawToolTip(spriteBatch, ToolTip, GUI.MouseOn.Rect, TooltipColorData); + DrawToolTip(spriteBatch, ToolTip, GUI.MouseOn.Rect, TooltipRichTextData); } - public static void DrawToolTip(SpriteBatch spriteBatch, string toolTip, Rectangle targetElement, List colorData = null) + public static void DrawToolTip(SpriteBatch spriteBatch, string toolTip, Rectangle targetElement, List richTextData = null) { if (Tutorials.Tutorial.ContentRunning) { return; } @@ -651,7 +663,7 @@ namespace Barotrauma if (toolTipBlock == null || (string)toolTipBlock.userData != toolTip) { - toolTipBlock = new GUITextBlock(new RectTransform(new Point(width, height), null), colorData, toolTip, font: GUI.SmallFont, wrap: true, style: "GUIToolTip"); + toolTipBlock = new GUITextBlock(new RectTransform(new Point(width, height), null), richTextData, toolTip, font: GUI.SmallFont, wrap: true, style: "GUIToolTip"); toolTipBlock.RectTransform.NonScaledSize = new Point( (int)(GUI.SmallFont.MeasureString(toolTipBlock.WrappedText).X + padding.X + toolTipBlock.Padding.X + toolTipBlock.Padding.Z), (int)(GUI.SmallFont.MeasureString(toolTipBlock.WrappedText).Y + padding.Y + toolTipBlock.Padding.Y + toolTipBlock.Padding.W)); @@ -787,8 +799,7 @@ namespace Barotrauma foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() == "conditional" && - !CheckConditional(subElement)) + if (subElement.Name.ToString().Equals("conditional", StringComparison.OrdinalIgnoreCase) && !CheckConditional(subElement)) { return null; } @@ -837,7 +848,7 @@ namespace Barotrauma { foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() == "conditional") { continue; } + if (subElement.Name.ToString().Equals("conditional", StringComparison.OrdinalIgnoreCase)) { continue; } FromXML(subElement, component is GUIListBox listBox ? listBox.Content.RectTransform : component.RectTransform); } @@ -1005,7 +1016,7 @@ namespace Barotrauma private static GUIFrame LoadGUIFrame(XElement element, RectTransform parent) { - string style = element.GetAttributeString("style", element.Name.ToString().ToLowerInvariant() == "spacing" ? null : ""); + string style = element.GetAttributeString("style", element.Name.ToString().Equals("spacing", StringComparison.OrdinalIgnoreCase) ? null : ""); if (style == "null") { style = null; } return new GUIFrame(RectTransform.Load(element, parent), style: style); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs index 9e950e020..913a701fd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIImage.cs @@ -1,12 +1,17 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; +using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; namespace Barotrauma { public class GUIImage : GUIComponent { + //paths of the textures that are being loaded asynchronously + private static readonly HashSet activeTextureLoads = new HashSet(); + public float Rotation; private Sprite sprite; @@ -15,7 +20,11 @@ namespace Barotrauma private bool crop; - private bool scaleToFit; + private readonly bool scaleToFit; + + private bool lazyLoaded, loading; + + public bool LoadAsynchronously; public bool Crop { @@ -75,7 +84,6 @@ namespace Barotrauma private GUIImage(RectTransform rectT, Sprite sprite, Rectangle? sourceRect, bool scaleToFit, string style) : base(style, rectT) { this.scaleToFit = scaleToFit; - sprite?.EnsureLazyLoaded(); Sprite = sprite; if (sourceRect.HasValue) { @@ -95,18 +103,44 @@ namespace Barotrauma } else { - rectT.SizeChanged += RecalculateScale; + if (Sprite != null && !Sprite.LazyLoad) + { + rectT.SizeChanged += RecalculateScale; + } } Enabled = true; } protected override void Draw(SpriteBatch spriteBatch) { - if (!Visible) return; + if (!Visible || loading) { return; } if (Parent != null) { State = Parent.State; } if (OverrideState != null) { State = OverrideState.Value; } + if (Sprite != null && Sprite.LazyLoad && !lazyLoaded) + { + if (LoadAsynchronously) + { + loading = true; + TaskPool.Add(LoadTextureAsync(), (Task) => + { + loading = false; + lazyLoaded = true; + RectTransform.SizeChanged += RecalculateScale; + RecalculateScale(); + }); + return; + } + else + { + Sprite.EnsureLazyLoaded(); + RectTransform.SizeChanged += RecalculateScale; + RecalculateScale(); + lazyLoaded = true; + } + } + Color currentColor = GetColor(State); if (BlendState != null) @@ -146,9 +180,52 @@ namespace Barotrauma private void RecalculateScale() { + if (sourceRect == Rectangle.Empty && sprite != null) + { + sourceRect = sprite.SourceRect; + } + Scale = sprite == null || sprite.SourceRect.Width == 0 || sprite.SourceRect.Height == 0 ? 1.0f : Math.Min(RectTransform.Rect.Width / (float)sprite.SourceRect.Width, RectTransform.Rect.Height / (float)sprite.SourceRect.Height); } + + private async Task LoadTextureAsync() + { + await Task.Yield(); + bool wait = true; + { + //if another GUIImage is already loading the same texture, wait for it to finish + while (wait) + { + await Task.Delay(5); + lock (activeTextureLoads) + { + wait = activeTextureLoads.Contains(Sprite.FullPath); + } + } + } + try + { + lock (activeTextureLoads) + { + activeTextureLoads.Add(Sprite.FullPath); + } + Sprite.EnsureLazyLoaded(); + } + finally + { + DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 10); + while (!Sprite.Loaded && DateTime.Now < timeOut) + { + await Task.Delay(5); + } + lock (activeTextureLoads) + { + activeTextureLoads.Remove(Sprite.FullPath); + } + } + return true; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs index 7658be341..41f708b39 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUILayoutGroup.cs @@ -76,7 +76,18 @@ namespace Barotrauma public GUILayoutGroup(RectTransform rectT, bool isHorizontal = false, Anchor childAnchor = Anchor.TopLeft) : base(null, rectT) { - CanBeFocused = false; +#if DEBUG + if (GameMain.DebugDraw) + { + CanBeFocused = true; + } + else + { +#endif + CanBeFocused = false; +#if DEBUG + } +#endif this.isHorizontal = isHorizontal; this.childAnchor = childAnchor; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs index 013c7513b..3852ecca1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIListBox.cs @@ -70,6 +70,11 @@ namespace Barotrauma scrollBarNeedsRecalculation = true; } } + + /// + /// true if mouse down should select elements instead of mouse up + /// + private bool useMouseDownToSelect = false; private Vector4? overridePadding; public Vector4 Padding @@ -80,7 +85,11 @@ namespace Barotrauma if (Style == null) { return Vector4.Zero; } return Style.Padding; } - set { overridePadding = value; } + set + { + dimensionsNeedsRecalculation = true; + overridePadding = value; + } } public GUIComponent SelectedComponent @@ -182,10 +191,11 @@ namespace Barotrauma public GUIComponent DraggedElement => draggedElement; /// For horizontal listbox, default side is on the bottom. For vertical, it's on the right. - public GUIListBox(RectTransform rectT, bool isHorizontal = false, Color? color = null, string style = "", bool isScrollBarOnDefaultSide = true) : base(style, rectT) + public GUIListBox(RectTransform rectT, bool isHorizontal = false, Color? color = null, string style = "", bool isScrollBarOnDefaultSide = true, bool useMouseDownToSelect = false) : base(style, rectT) { CanBeFocused = true; selected = new List(); + this.useMouseDownToSelect = useMouseDownToSelect; ContentBackground = new GUIFrame(new RectTransform(Vector2.One, rectT), style) { CanBeFocused = false @@ -237,7 +247,7 @@ namespace Barotrauma UpdateDimensions(); } - private void UpdateDimensions() + public void UpdateDimensions() { dimensionsNeedsRecalculation = false; ContentBackground.RectTransform.Resize(Rect.Size); @@ -403,7 +413,10 @@ namespace Barotrauma if (Enabled && CanBeFocused && child.CanBeFocused && (GUI.IsMouseOn(child)) && child.Rect.Contains(PlayerInput.MousePosition)) { child.State = ComponentState.Hover; - if (PlayerInput.PrimaryMouseButtonClicked()) + + var mouseDown = useMouseDownToSelect ? PlayerInput.PrimaryMouseButtonDown() : PlayerInput.PrimaryMouseButtonClicked(); + + if (mouseDown) { Select(i, autoScroll: false); } @@ -426,7 +439,7 @@ namespace Barotrauma } else { - child.State = ComponentState.None; + child.State = !child.ExternalHighlight ? ComponentState.None : ComponentState.Hover; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs index 9eb0be459..b9091d17c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIMessageBox.cs @@ -249,7 +249,10 @@ namespace Barotrauma InnerFrame.RectTransform.AbsoluteOffset = Vector2.SmoothStep(initialPos, defaultPos, openState).ToPoint(); openState = Math.Min(openState + deltaTime * 2.0f, 1.0f); - inGameCloseTimer += deltaTime; + if (GUI.MouseOn != InnerFrame && !InnerFrame.IsParentOf(GUI.MouseOn)) + { + inGameCloseTimer += deltaTime; + } if (inGameCloseTimer >= inGameCloseTime) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs index 96191e3be..75ddeeb79 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUIStyle.cs @@ -59,11 +59,41 @@ namespace Barotrauma /// public Color Blue { get; private set; } = Color.Blue; + /// + /// General yellow color used for elements whose colors are set from code + /// + public Color Yellow { get; private set; } = Color.Yellow; + + public Color ColorInventoryEmpty { get; private set; } = Color.Red; + public Color ColorInventoryHalf { get; private set; } = Color.Orange; + public Color ColorInventoryFull { get; private set; } = Color.LightGreen; + public Color ColorInventoryBackground { get; private set; } = Color.Gray; + public Color TextColor { get; private set; } = Color.White * 0.8f; public Color TextColorBright { get; private set; } = Color.White * 0.9f; public Color TextColorDark { get; private set; } = Color.Black * 0.9f; public Color TextColorDim { get; private set; } = Color.White * 0.6f; + // Inventory + public Color EquipmentSlotIconColor { get; private set; } = new Color(99, 70, 64); + + // Health HUD + public Color BuffColorLow { get; private set; } = Color.LightGreen; + public Color BuffColorMedium { get; private set; } = Color.Green; + public Color BuffColorHigh { get; private set; } = Color.DarkGreen; + + public Color DebuffColorLow { get; private set; } = Color.DarkSalmon; + public Color DebuffColorMedium { get; private set; } = Color.Red; + public Color DebuffColorHigh { get; private set; } = Color.DarkRed; + + public Color HealthBarColorLow { get; private set; } = Color.Red; + public Color HealthBarColorMedium { get; private set; } = Color.Orange; + public Color HealthBarColorHigh { get; private set; } = new Color(78, 114, 88); + + public Color EquipmentIndicatorNotEquipped { get; private set; } = Color.Gray; + public Color EquipmentIndicatorEquipped { get; private set; } = new Color(105, 202, 125); + public Color EquipmentIndicatorRunningOut { get; private set; } = new Color(202, 105, 105); + public static Point ItemFrameMargin => new Point(50, 56).Multiply(GUI.SlicedSpriteScale); public static Point ItemFrameOffset => new Point(0, 3).Multiply(GUI.SlicedSpriteScale); @@ -103,10 +133,25 @@ namespace Barotrauma case "blue": Blue = subElement.GetAttributeColor("color", Blue); break; + case "yellow": + Yellow = subElement.GetAttributeColor("color", Yellow); + break; + case "colorinventoryempty": + ColorInventoryEmpty = subElement.GetAttributeColor("color", ColorInventoryEmpty); + break; + case "colorinventoryhalf": + ColorInventoryHalf = subElement.GetAttributeColor("color", ColorInventoryHalf); + break; + case "colorinventoryfull": + ColorInventoryFull = subElement.GetAttributeColor("color", ColorInventoryFull); + break; + case "colorinventorybackground": + ColorInventoryBackground = subElement.GetAttributeColor("color", ColorInventoryBackground); + break; case "textcolordark": TextColorDark = subElement.GetAttributeColor("color", TextColorDark); break; - case "TextColorBright": + case "textcolorbright": TextColorBright = subElement.GetAttributeColor("color", TextColorBright); break; case "textcolordim": @@ -116,6 +161,45 @@ namespace Barotrauma case "textcolor": TextColor = subElement.GetAttributeColor("color", TextColor); break; + case "equipmentsloticoncolor": + EquipmentSlotIconColor = subElement.GetAttributeColor("color", EquipmentSlotIconColor); + break; + case "buffcolorlow": + BuffColorLow = subElement.GetAttributeColor("color", BuffColorLow); + break; + case "buffcolormedium": + BuffColorMedium = subElement.GetAttributeColor("color", BuffColorMedium); + break; + case "buffcolorhigh": + BuffColorHigh = subElement.GetAttributeColor("color", BuffColorHigh); + break; + case "debuffcolorlow": + DebuffColorLow = subElement.GetAttributeColor("color", DebuffColorLow); + break; + case "debuffcolormedium": + DebuffColorMedium = subElement.GetAttributeColor("color", DebuffColorMedium); + break; + case "debuffcolorhigh": + DebuffColorHigh = subElement.GetAttributeColor("color", DebuffColorHigh); + break; + case "healthbarcolorlow": + HealthBarColorLow = subElement.GetAttributeColor("color", HealthBarColorLow); + break; + case "healthbarcolormedium": + HealthBarColorMedium = subElement.GetAttributeColor("color", HealthBarColorMedium); + break; + case "healthbarcolorhigh": + HealthBarColorHigh = subElement.GetAttributeColor("color", HealthBarColorHigh); + break; + case "equipmentindicatornotequipped": + EquipmentIndicatorNotEquipped = subElement.GetAttributeColor("color", EquipmentIndicatorNotEquipped); + break; + case "equipmentindicatorequipped": + EquipmentIndicatorEquipped = subElement.GetAttributeColor("color", EquipmentIndicatorEquipped); + break; + case "equipmentindicatorrunningout": + EquipmentIndicatorRunningOut = subElement.GetAttributeColor("color", EquipmentIndicatorRunningOut); + break; case "uiglow": UIGlow = new UISprite(subElement); break; @@ -250,9 +334,8 @@ namespace Barotrauma //check if any of the language override fonts want to override the font size as well foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "override") { continue; } - string language = subElement.GetAttributeString("language", "").ToLowerInvariant(); - if (GameMain.Config.Language.ToLowerInvariant() == language) + if (!subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { continue; } + if (GameMain.Config.Language.Equals(subElement.GetAttributeString("language", ""), StringComparison.OrdinalIgnoreCase)) { uint overrideFontSize = GetFontSize(subElement, 0); if (overrideFontSize > 0) { return overrideFontSize; } @@ -261,7 +344,7 @@ namespace Barotrauma foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "size") { continue; } + if (!subElement.Name.ToString().Equals("size", StringComparison.OrdinalIgnoreCase)) { continue; } Point maxResolution = subElement.GetAttributePoint("maxresolution", new Point(int.MaxValue, int.MaxValue)); if (GameMain.GraphicsWidth <= maxResolution.X && GameMain.GraphicsHeight <= maxResolution.Y) { @@ -275,9 +358,8 @@ namespace Barotrauma { foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "override") { continue; } - string language = subElement.GetAttributeString("language", "").ToLowerInvariant(); - if (GameMain.Config.Language.ToLowerInvariant() == language) + if (!subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { continue; } + if (GameMain.Config.Language.Equals(subElement.GetAttributeString("language", ""), StringComparison.OrdinalIgnoreCase)) { return subElement.GetAttributeString("file", ""); } @@ -289,9 +371,8 @@ namespace Barotrauma { foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "override") { continue; } - string language = subElement.GetAttributeString("language", "").ToLowerInvariant(); - if (GameMain.Config.Language.ToLowerInvariant() == language) + if (!subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { continue; } + if (GameMain.Config.Language.Equals(subElement.GetAttributeString("language", ""), StringComparison.OrdinalIgnoreCase)) { return subElement.GetAttributeBool("dynamicloading", false); } @@ -303,9 +384,8 @@ namespace Barotrauma { foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "override") { continue; } - string language = subElement.GetAttributeString("language", "").ToLowerInvariant(); - if (GameMain.Config.Language.ToLowerInvariant() == language) + if (!subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { continue; } + if (GameMain.Config.Language.Equals(subElement.GetAttributeString("language", ""), StringComparison.OrdinalIgnoreCase)) { return subElement.GetAttributeBool("iscjk", false); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs index 73daf81f0..faedb8243 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs @@ -238,9 +238,40 @@ namespace Barotrauma get { return censoredText; } } - private List colorData = null; - private bool hasColorHighlight = false; - + public class StrikethroughSettings + { + private Color color = GUI.Style.Red; + private int thickness; + private int expand; + + public StrikethroughSettings(Color? color = null, int thickness = 1, int expand = 0) + { + if (color != null) this.color = color.Value; + this.thickness = thickness; + this.expand = expand; + } + + public void Draw(SpriteBatch spriteBatch, float textSizeHalf, float xPos, float yPos) + { + ShapeExtensions.DrawLine(spriteBatch, new Vector2(xPos - textSizeHalf - expand, yPos), new Vector2(xPos + textSizeHalf + expand, yPos), color, thickness); + } + } + + public StrikethroughSettings Strikethrough = null; + + private readonly List richTextData = null; + + private readonly bool hasColorHighlight = false; + + public struct ClickableArea + { + public RichTextData Data; + + public delegate void OnClickDelegate(GUITextBlock textBlock, ClickableArea area); + public OnClickDelegate OnClick; + } + public List ClickableAreas { get; private set; } = new List(); + /// /// This is the new constructor. /// If the rectT height is set 0, the height is calculated from the text. @@ -282,11 +313,11 @@ namespace Barotrauma Enabled = true; Censor = false; } - public GUITextBlock(RectTransform rectT, List colorData, string text, Color? textColor = null, ScalableFont font = null, Alignment textAlignment = Alignment.Left, bool wrap = false, string style = "", Color? color = null, bool playerInput = false) + public GUITextBlock(RectTransform rectT, List richTextData, string text, Color? textColor = null, ScalableFont font = null, Alignment textAlignment = Alignment.Left, bool wrap = false, string style = "", Color? color = null, bool playerInput = false) : this(rectT, text, textColor, font, textAlignment, wrap, style, color, playerInput) { - this.colorData = colorData; - hasColorHighlight = colorData != null; + this.richTextData = richTextData; + hasColorHighlight = richTextData != null; } public void CalculateHeightFromText(int padding = 0) @@ -427,6 +458,129 @@ namespace Barotrauma disabledTextColor = color; } + protected List> GetAllPositions() + { + float halfHeight = Font.MeasureString("T").Y * 0.5f; + string textDrawn = Censor ? CensoredText : WrappedText; + var positions = new List>(); + if (textDrawn.Contains("\n")) + { + string[] lines = textDrawn.Split('\n'); + int index = 0; + int totalIndex = 0; + for (int i = 0; i < lines.Length; i++) + { + string line = lines[i]; + totalIndex += line.Length; + float totalTextHeight = Font.MeasureString(textDrawn.Substring(0, totalIndex)).Y; + for (int j = 0; j <= line.Length; j++) + { + Vector2 lineTextSize = Font.MeasureString(line.Substring(0, j)); + Vector2 indexPos = new Vector2(lineTextSize.X + Padding.X, totalTextHeight + Padding.Y - halfHeight); + //DebugConsole.NewMessage($"index: {index}, pos: {indexPos}", Color.AliceBlue); + positions.Add(new Tuple(indexPos, index + j)); + } + index = totalIndex; + } + } + else + { + textDrawn = Censor ? CensoredText : Text; + for (int i = 0; i <= Text.Length; i++) + { + Vector2 textSize = Font.MeasureString(textDrawn.Substring(0, i)); + Vector2 indexPos = new Vector2(textSize.X + Padding.X, textSize.Y + Padding.Y - halfHeight) + TextPos - Origin; + //DebugConsole.NewMessage($"index: {i}, pos: {indexPos}", Color.WhiteSmoke); + positions.Add(new Tuple(indexPos, i)); + } + } + return positions; + } + + public int GetCaretIndexFromScreenPos(Vector2 pos) + { + return GetCaretIndexFromLocalPos(pos - Rect.Location.ToVector2()); + } + + public int GetCaretIndexFromLocalPos(Vector2 pos) + { + var positions = GetAllPositions(); + if (positions.Count == 0) { return 0; } + float halfHeight = Font.MeasureString("T").Y * 0.5f; + + var currPosition = positions[0]; + + for (int i = 1; i < positions.Count; i++) + { + var p1 = positions[i]; + var p2 = currPosition; + + float diffY = Math.Abs(p1.Item1.Y - pos.Y) - Math.Abs(p2.Item1.Y - pos.Y); + if (diffY < -3.0f) + { + currPosition = p1; continue; + } + else if (diffY > 3.0f) + { + continue; + } + else + { + diffY = Math.Abs(p1.Item1.Y - pos.Y); + if (diffY < halfHeight) + { + //we are on this line, select the nearest character + float diffX = Math.Abs(p1.Item1.X - pos.X) - Math.Abs(p2.Item1.X - pos.X); + if (diffX < -1.0f) + { + currPosition = p1; continue; + } + else + { + continue; + } + } + else + { + //we are on a different line, preserve order + if (p1.Item2 < p2.Item2) + { + if (p1.Item1.Y > pos.Y) { currPosition = p1; } + } + else if (p1.Item2 > p2.Item2) + { + if (p1.Item1.Y < pos.Y) { currPosition = p1; } + } + continue; + } + } + } + //GUI.AddMessage($"index: {posIndex.Item2}, pos: {posIndex.Item1}", Color.WhiteSmoke); + return currPosition != null ? currPosition.Item2 : Text.Length; + } + + protected override void Update(float deltaTime) + { + base.Update(deltaTime); + + if (ClickableAreas.Any() && (GUI.MouseOn?.IsParentOf(this) ?? true) && Rect.Contains(PlayerInput.MousePosition)) + { + int index = GetCaretIndexFromScreenPos(PlayerInput.MousePosition); + foreach (ClickableArea clickableArea in ClickableAreas) + { + if (clickableArea.Data.StartIndex <= index && index <= clickableArea.Data.EndIndex) + { + GUI.MouseCursor = CursorState.Hand; + if (PlayerInput.PrimaryMouseButtonClicked()) + { + clickableArea.OnClick?.Invoke(this, clickableArea); + } + break; + } + } + } + } + protected override void Draw(SpriteBatch spriteBatch) { if (!Visible) { return; } @@ -480,7 +634,12 @@ namespace Barotrauma else { Font.DrawStringWithColors(spriteBatch, Censor ? censoredText : (Wrap ? wrappedText : text), pos, - currentTextColor * (currentTextColor.A / 255.0f), 0.0f, origin, TextScale, SpriteEffects.None, textDepth, colorData); + currentTextColor * (currentTextColor.A / 255.0f), 0.0f, origin, TextScale, SpriteEffects.None, textDepth, richTextData); + } + + if (Strikethrough != null) + { + Strikethrough.Draw(spriteBatch, (int)Math.Ceiling(TextSize.X / 2f), pos.X, ForceUpperCase ? pos.Y : pos.Y + GUI.Scale * 2f); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs index c2aacfb6e..93be1de8d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBox.cs @@ -68,6 +68,14 @@ namespace Barotrauma private Vector2 selectionRectSize; private readonly Memento memento = new Memento(); + + // Skip one update cycle, fixes Enter key instantly deselecting the chatbox + private bool skipUpdate; + + public GUIFrame Frame + { + get { return frame; } + } public GUITextBlock.TextGetterHandler TextGetter { @@ -357,114 +365,14 @@ namespace Barotrauma caretPosDirty = false; } - protected List> GetAllPositions() - { - float halfHeight = Font.MeasureString("T").Y * 0.5f; - string textDrawn = Censor ? textBlock.CensoredText : textBlock.WrappedText; - var positions = new List>(); - if (textDrawn.Contains("\n")) - { - string[] lines = textDrawn.Split('\n'); - int index = 0; - int totalIndex = 0; - for (int i = 0; i < lines.Length; i++) - { - string line = lines[i]; - totalIndex += line.Length; - float totalTextHeight = Font.MeasureString(textDrawn.Substring(0, totalIndex)).Y; - for (int j = 0; j <= line.Length; j++) - { - Vector2 lineTextSize = Font.MeasureString(line.Substring(0, j)); - Vector2 indexPos = new Vector2(lineTextSize.X + textBlock.Padding.X, totalTextHeight + textBlock.Padding.Y - halfHeight); - //DebugConsole.NewMessage($"index: {index}, pos: {indexPos}", Color.AliceBlue); - positions.Add(new Tuple(indexPos, index + j)); - } - index = totalIndex; - } - } - else - { - textDrawn = Censor ? textBlock.CensoredText : textBlock.Text; - for (int i = 0; i <= textBlock.Text.Length; i++) - { - Vector2 textSize = Font.MeasureString(textDrawn.Substring(0, i)); - Vector2 indexPos = new Vector2(textSize.X + textBlock.Padding.X, textSize.Y + textBlock.Padding.Y - halfHeight) + textBlock.TextPos - textBlock.Origin; - //DebugConsole.NewMessage($"index: {i}, pos: {indexPos}", Color.WhiteSmoke); - positions.Add(new Tuple(indexPos, i)); - } - } - return positions; - } - - public int GetCaretIndexFromScreenPos(Vector2 pos) - { - return GetCaretIndexFromLocalPos(pos - textBlock.Rect.Location.ToVector2()); - } - - public int GetCaretIndexFromLocalPos(Vector2 pos) - { - var positions = GetAllPositions(); - if (positions.Count==0) { return 0; } - float halfHeight = Font.MeasureString("T").Y * 0.5f; - - var currPosition = positions[0]; - - for (int i=1;i 3.0f) - { - continue; - } - else - { - diffY = Math.Abs(p1.Item1.Y - pos.Y); - if (diffY < halfHeight) - { - //we are on this line, select the nearest character - float diffX = Math.Abs(p1.Item1.X - pos.X) - Math.Abs(p2.Item1.X - pos.X); - if (diffX < -1.0f) - { - currPosition = p1; continue; - } - else - { - continue; - } - } - else - { - //we are on a different line, preserve order - if (p1.Item2 < p2.Item2) - { - if (p1.Item1.Y > pos.Y) { currPosition = p1; } - } - else if (p1.Item2 > p2.Item2) - { - if (p1.Item1.Y < pos.Y) { currPosition = p1; } - } - continue; - } - } - } - //GUI.AddMessage($"index: {posIndex.Item2}, pos: {posIndex.Item1}", Color.WhiteSmoke); - return currPosition != null ? currPosition.Item2 : textBlock.Text.Length; - } - public void Select(int forcedCaretIndex = -1) { + skipUpdate = true; if (memento.Current == null) { memento.Store(Text); } - CaretIndex = forcedCaretIndex == - 1 ? GetCaretIndexFromScreenPos(PlayerInput.MousePosition) : forcedCaretIndex; + CaretIndex = forcedCaretIndex == - 1 ? textBlock.GetCaretIndexFromScreenPos(PlayerInput.MousePosition) : forcedCaretIndex; ClearSelection(); selected = true; GUI.KeyboardDispatcher.Subscriber = this; @@ -494,6 +402,13 @@ namespace Barotrauma if (flashTimer > 0.0f) flashTimer -= deltaTime; if (!Enabled) { return; } + + if (skipUpdate) + { + skipUpdate = false; + return; + } + if (MouseRect.Contains(PlayerInput.MousePosition) && (GUI.MouseOn == null || (!(GUI.MouseOn is GUIButton) && GUI.IsMouseOn(this)))) { State = ComponentState.Hover; @@ -513,7 +428,7 @@ namespace Barotrauma { if (!MathUtils.NearlyEqual(PlayerInput.MouseSpeed.X, 0)) { - CaretIndex = GetCaretIndexFromScreenPos(PlayerInput.MousePosition); + CaretIndex = textBlock.GetCaretIndexFromScreenPos(PlayerInput.MousePosition); CalculateCaretPos(); CalculateSelection(); } @@ -788,7 +703,7 @@ namespace Barotrauma InitSelectionStart(); } float lineHeight = Font.MeasureString("T").Y; - int newIndex = GetCaretIndexFromLocalPos(new Vector2(caretPos.X, caretPos.Y-lineHeight)); + int newIndex = textBlock.GetCaretIndexFromLocalPos(new Vector2(caretPos.X, caretPos.Y-lineHeight)); CaretIndex = newIndex; caretTimer = 0; HandleSelection(); @@ -799,7 +714,7 @@ namespace Barotrauma InitSelectionStart(); } lineHeight = Font.MeasureString("T").Y; - newIndex = GetCaretIndexFromLocalPos(new Vector2(caretPos.X, caretPos.Y+lineHeight)); + newIndex = textBlock.GetCaretIndexFromLocalPos(new Vector2(caretPos.X, caretPos.Y+lineHeight)); CaretIndex = newIndex; caretTimer = 0; HandleSelection(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs index a67480d28..8df88614e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/HUDLayoutSettings.cs @@ -112,16 +112,16 @@ namespace Barotrauma //slice from the top of the screen for misc buttons (info, end round, server controls) ButtonAreaTop = new Rectangle(Padding, Padding, GameMain.GraphicsWidth - Padding * 2, (int)(50 * GUI.Scale)); - int infoAreaWidth = (int)(142 * GUI.Scale * CharacterInfo.BgScale); - int infoAreaHeight = (int)(98 * GUI.Scale * CharacterInfo.BgScale); - int portraitSize = (int)(125 * GUI.Scale); + int infoAreaWidth = (int)(142 * GUI.Scale); + int infoAreaHeight = (int)(98 * GUI.Scale); + int portraitSize = (int)(infoAreaHeight * 0.95f); BottomRightInfoArea = new Rectangle(GameMain.GraphicsWidth - Padding * 2 - infoAreaWidth, GameMain.GraphicsHeight - Padding * 2 - infoAreaHeight, infoAreaWidth, infoAreaHeight); - PortraitArea = new Rectangle(GameMain.GraphicsWidth - Padding - portraitSize, GameMain.GraphicsHeight - Padding - portraitSize, portraitSize, portraitSize); + PortraitArea = new Rectangle(GameMain.GraphicsWidth - portraitSize, BottomRightInfoArea.Bottom - portraitSize + Padding / 2, portraitSize, portraitSize); //horizontal slices at the corners of the screen for health bar and affliction icons int afflictionAreaHeight = (int)(50 * GUI.Scale); - int healthBarWidth = (int)((BottomRightInfoArea.Width + CharacterInventory.SlotSize.X + CharacterInventory.Spacing) * 1.1f); - int healthBarHeight = (int)Math.Max(50f * GUI.Scale, 25f); + int healthBarWidth = BottomRightInfoArea.Width + CharacterInventory.SlotSize.X + CharacterInventory.Spacing * 2 + CharacterInventory.HideButtonWidth; + int healthBarHeight = (int)(50f * GUI.Scale); HealthBarArea = new Rectangle(BottomRightInfoArea.X - (healthBarWidth - BottomRightInfoArea.Width) + (int)(2 * GUI.Scale), BottomRightInfoArea.Y - healthBarHeight + (int)(10 * GUI.Scale), healthBarWidth, healthBarHeight); AfflictionAreaLeft = new Rectangle(HealthBarArea.X, HealthBarArea.Y - Padding - afflictionAreaHeight, HealthBarArea.Width, afflictionAreaHeight); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs index f730a55f2..79b77f528 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/LoadingScreen.cs @@ -338,6 +338,8 @@ namespace Barotrauma PendingSplashScreens.Clear(); currSplashScreen = null; } + + if (currSplashScreen == null) { return; } } if (currSplashScreen.IsPlaying) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs index d61cc17e9..d810b720d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/RectTransform.cs @@ -651,6 +651,13 @@ namespace Barotrauma Parent.ChildrenChanged?.Invoke(this); } + public void ReverseChildren() + { + children.Reverse(); + RecalculateAll(false, false, true); + Parent.ChildrenChanged?.Invoke(this); + } + public void SetAsLastChild() { if (IsLastChild) { return; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs new file mode 100644 index 000000000..22fa899be --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -0,0 +1,932 @@ +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System.Linq; +using Barotrauma.Networking; + +namespace Barotrauma +{ + class TabMenu + { + public static bool PendingChanges = false; + + private static bool initialized = false; + + private static UISprite spectateIcon, deadIcon, disconnectedIcon; + private static Sprite ownerIcon, moderatorIcon; + + private enum InfoFrameTab { Crew, Mission, MyCharacter, Traitor }; + private static InfoFrameTab selectedTab; + private GUIFrame infoFrame, contentFrame; + + private readonly List tabButtons = new List(); + private GUIFrame infoFrameHolder; + private List linkedGUIList; + private GUIListBox logList; + private GUIListBox[] crewListArray; + private float sizeMultiplier = 1f; + + private IEnumerable crew; + private List teamIDs; + private const string inLobbyString = "\u2022 \u2022 \u2022"; + + private static Color ownCharacterBGColor = Color.Gold * 0.7f; + + private class LinkedGUI + { + private const ushort lowPingThreshold = 100; + private const ushort mediumPingThreshold = 200; + + private ushort currentPing; + private Client client; + private Character character; + private bool hasCharacter; + private GUITextBlock textBlock; + private GUIFrame frame; + + public LinkedGUI(Client client, GUIFrame frame, bool hasCharacter, GUITextBlock textBlock) + { + this.client = client; + this.textBlock = textBlock; + this.frame = frame; + this.hasCharacter = hasCharacter; + } + + public LinkedGUI(Character character, GUIFrame frame, bool hasCharacter, GUITextBlock textBlock) + { + this.character = character; + this.textBlock = textBlock; + this.frame = frame; + this.hasCharacter = hasCharacter; + } + + public bool HasMultiplayerCharacterChanged() + { + if (client == null) return false; + bool characterState = client.Character != null; + if (characterState && client.Character.IsDead) characterState = false; + return hasCharacter != characterState; + } + + public bool HasMultiplayerCharacterDied() + { + if (client == null || !hasCharacter || client.Character == null) return false; + return client.Character.IsDead; + } + + public bool HasAICharacterDied() + { + if (character == null) return false; + return character.IsDead; + } + + public void TryPingRefresh() + { + if (client == null) return; + if (currentPing == client.Ping) return; + currentPing = client.Ping; + textBlock.Text = currentPing.ToString(); + textBlock.TextColor = GetPingColor(); + } + + private Color GetPingColor() + { + if (currentPing < lowPingThreshold) + { + return GUI.Style.Green; + } + else if (currentPing < mediumPingThreshold) + { + return GUI.Style.Yellow; + } + else + { + return GUI.Style.Red; + } + } + + public void Remove(GUIFrame parent) + { + parent.RemoveChild(frame); + } + } + + public void Initialize() + { + spectateIcon = GUI.Style.GetComponentStyle("SpectateIcon").Sprites[GUIComponent.ComponentState.None][0]; + deadIcon = GUI.Style.GetComponentStyle("DeadIcon").Sprites[GUIComponent.ComponentState.None][0]; + disconnectedIcon = GUI.Style.GetComponentStyle("DisconnectedIcon").Sprites[GUIComponent.ComponentState.None][0]; + ownerIcon = GUI.Style.GetComponentStyle("OwnerIcon").Sprites[GUIComponent.ComponentState.None][0].Sprite; + moderatorIcon = GUI.Style.GetComponentStyle("ModeratorIcon").Sprites[GUIComponent.ComponentState.None][0].Sprite; + initialized = true; + } + + public TabMenu() + { + if (!initialized) Initialize(); + + CreateInfoFrame(selectedTab); + SelectInfoFrameTab(null, selectedTab); + } + + public void Update() + { + if (selectedTab != InfoFrameTab.Crew) return; + if (linkedGUIList == null) return; + + if (GameMain.IsMultiplayer) + { + for (int i = 0; i < linkedGUIList.Count; i++) + { + linkedGUIList[i].TryPingRefresh(); + if (linkedGUIList[i].HasMultiplayerCharacterChanged() || linkedGUIList[i].HasMultiplayerCharacterDied() || linkedGUIList[i].HasAICharacterDied()) + { + RemoveCurrentElements(); + CreateMultiPlayerList(true); + return; + } + } + } + else + { + for (int i = 0; i < linkedGUIList.Count; i++) + { + if (linkedGUIList[i].HasAICharacterDied()) + { + RemoveCurrentElements(); + CreateSinglePlayerList(true); + } + } + } + } + + public void AddToGUIUpdateList() + { + infoFrame?.AddToGUIUpdateList(); + NetLobbyScreen.JobInfoFrame?.AddToGUIUpdateList(); + } + + public static void OnRoundEnded() + { + storedMessages.Clear(); + PendingChanges = false; + } + + private void CreateInfoFrame(InfoFrameTab selectedTab) + { + tabButtons.Clear(); + + infoFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), style: "GUIBackgroundBlocker"); + + switch (selectedTab) + { + case InfoFrameTab.Crew: + case InfoFrameTab.Mission: + case InfoFrameTab.Traitor: + default: + contentFrame = new GUIFrame(new RectTransform(new Vector2(0.33f, 0.667f), infoFrame.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { /*MinSize = new Point(width, height),*/ RelativeOffset = new Vector2(0.025f, 0.12f) }); + break; + case InfoFrameTab.MyCharacter: + contentFrame = new GUIFrame(new RectTransform(new Vector2(0.33f, 0.5f), infoFrame.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { /*MinSize = new Point(width, height),*/ RelativeOffset = new Vector2(0.025f, 0.12f) }); + break; + } + + var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.958f, 0.943f), contentFrame.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { AbsoluteOffset = new Point(0, GUI.IntScale(17.5f)) }, style: null); + var buttonArea = new GUILayoutGroup(new RectTransform(new Point(innerFrame.Rect.Width, GUI.IntScale(25f)), innerFrame.RectTransform) { AbsoluteOffset = new Point(2, 0) }, isHorizontal: true) + { + RelativeSpacing = 0.01f + }; + + infoFrameHolder = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.926f), innerFrame.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter), style: null); + + var crewButton = new GUIButton(new RectTransform(new Vector2(0.245f, 1.0f), buttonArea.RectTransform), TextManager.Get("Crew"), style: "GUITabButton") + { + UserData = InfoFrameTab.Crew, + OnClicked = SelectInfoFrameTab + }; + tabButtons.Add(crewButton); + + var missionButton = new GUIButton(new RectTransform(new Vector2(0.245f, 1.0f), buttonArea.RectTransform), TextManager.Get("Mission"), style: "GUITabButton") + { + UserData = InfoFrameTab.Mission, + OnClicked = SelectInfoFrameTab + }; + tabButtons.Add(missionButton); + + bool isTraitor = GameMain.Client?.Character?.IsTraitor ?? false; + if (isTraitor && GameMain.Client.TraitorMission != null) + { + var traitorButton = new GUIButton(new RectTransform(new Vector2(0.245f, 1.0f), buttonArea.RectTransform), TextManager.Get("tabmenu.traitor"), style: "GUITabButton") + { + UserData = InfoFrameTab.Traitor, + OnClicked = SelectInfoFrameTab + }; + tabButtons.Add(traitorButton); + } + + if (GameMain.NetworkMember != null) + { + var myCharacterButton = new GUIButton(new RectTransform(new Vector2(0.245f, 1.0f), buttonArea.RectTransform), TextManager.Get("tabmenu.character"), style: "GUITabButton") + { + UserData = InfoFrameTab.MyCharacter, + OnClicked = SelectInfoFrameTab + }; + tabButtons.Add(myCharacterButton); + } + } + + private bool SelectInfoFrameTab(GUIButton button, object userData) + { + selectedTab = (InfoFrameTab)userData; + + CreateInfoFrame(selectedTab); + tabButtons.ForEach(tb => tb.Selected = (InfoFrameTab)tb.UserData == selectedTab); + + switch (selectedTab) + { + case InfoFrameTab.Crew: + CreateCrewListFrame(infoFrameHolder); + break; + case InfoFrameTab.Mission: + CreateMissionInfo(infoFrameHolder); + break; + case InfoFrameTab.Traitor: + TraitorMissionPrefab traitorMission = GameMain.Client.TraitorMission; + Character traitor = GameMain.Client.Character; + if (traitor == null || traitorMission == null) return false; + CreateTraitorInfo(infoFrameHolder, traitorMission, traitor); + break; + case InfoFrameTab.MyCharacter: + if (GameMain.NetworkMember == null) { return false; } + GameMain.NetLobbyScreen.CreatePlayerFrame(infoFrameHolder); + break; + } + + return true; + } + + private const float jobColumnWidthPercentage = 0.138f; + private const float characterColumnWidthPercentage = 0.656f; + private const float pingColumnWidthPercentage = 0.206f; + + private int jobColumnWidth, characterColumnWidth, pingColumnWidth; + + private void CreateCrewListFrame(GUIFrame crewFrame) + { + crew = GameMain.GameSession.CrewManager.GetCharacters(); + teamIDs = crew.Select(c => c.TeamID).Distinct().ToList(); + + // Show own team first when there's more than one team + if (teamIDs.Count > 1 && GameMain.Client.Character != null) + { + Character.TeamType ownTeam = GameMain.Client.Character.TeamID; + teamIDs = teamIDs.OrderBy(i => i != ownTeam).ThenBy(i => i).ToList(); + } + + if (!teamIDs.Any()) teamIDs.Add(Character.TeamType.None); + + var content = new GUILayoutGroup(new RectTransform(Vector2.One, crewFrame.RectTransform)); + + crewListArray = new GUIListBox[teamIDs.Count]; + GUILayoutGroup[] headerFrames = new GUILayoutGroup[teamIDs.Count]; + + float nameHeight = 0.075f; + + Vector2 crewListSize = new Vector2(1f, 1f / teamIDs.Count - (teamIDs.Count > 1 ? nameHeight * 1.1f : 0f)); + for (int i = 0; i < teamIDs.Count; i++) + { + if (teamIDs.Count > 1) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, nameHeight), content.RectTransform), CombatMission.GetTeamName(teamIDs[i]), textColor: i == 0 ? GUI.Style.Green : GUI.Style.Orange) { ForceUpperCase = true }; + } + + headerFrames[i] = new GUILayoutGroup(new RectTransform(Vector2.Zero, content.RectTransform, Anchor.TopLeft, Pivot.BottomLeft) { AbsoluteOffset = new Point(2, -1) }, isHorizontal: true) + { + AbsoluteSpacing = 2, + UserData = i + }; + + GUIListBox crewList = new GUIListBox(new RectTransform(crewListSize, content.RectTransform)) + { + Padding = new Vector4(2, 5, 0, 0), + AutoHideScrollBar = false + }; + crewList.UpdateDimensions(); + + if (teamIDs.Count > 1) + { + crewList.OnSelected = (component, obj) => + { + for (int i = 0; i < crewListArray.Length; i++) + { + if (crewListArray[i] == crewList) continue; + crewListArray[i].Deselect(); + } + SelectElement(component.UserData, crewList); + return true; + }; + } + else + { + crewList.OnSelected = (component, obj) => + { + SelectElement(component.UserData, crewList); + return true; + }; + } + + crewListArray[i] = crewList; + } + + for (int i = 0; i < teamIDs.Count; i++) + { + headerFrames[i].RectTransform.RelativeSize = new Vector2(1f - crewListArray[i].ScrollBar.Rect.Width / (float)crewListArray[i].Rect.Width, GUI.HotkeyFont.Size / (float)crewFrame.RectTransform.Rect.Height * 1.5f); + + if (!GameMain.IsMultiplayer) + { + CreateSinglePlayerListContentHolder(headerFrames[i]); + } + else + { + CreateMultiPlayerListContentHolder(headerFrames[i]); + } + } + + crewFrame.RectTransform.AbsoluteOffset = new Point(0, (int)(headerFrames[0].Rect.Height * headerFrames.Length) - (teamIDs.Count > 1 ? GUI.IntScale(10f) : 0)); + + if (GameMain.IsMultiplayer) + { + CreateMultiPlayerList(false); + CreateMultiPlayerLogContent(crewFrame); + } + else + { + CreateSinglePlayerList(false); + } + } + + private void CreateSinglePlayerListContentHolder(GUILayoutGroup headerFrame) + { + GUIButton jobButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("tabmenu.job"), style: "GUIButtonSmallFreeScale"); + GUIButton characterButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("name"), style: "GUIButtonSmallFreeScale"); + + sizeMultiplier = (headerFrame.Rect.Width - headerFrame.AbsoluteSpacing * (headerFrame.CountChildren - 1)) / (float)headerFrame.Rect.Width; + + jobButton.RectTransform.RelativeSize = new Vector2(jobColumnWidthPercentage * sizeMultiplier, 1f); + characterButton.RectTransform.RelativeSize = new Vector2((1f - jobColumnWidthPercentage * sizeMultiplier) * sizeMultiplier, 1f); + + jobButton.TextBlock.Font = characterButton.TextBlock.Font = GUI.HotkeyFont; + jobButton.CanBeFocused = characterButton.CanBeFocused = false; + jobButton.TextBlock.ForceUpperCase = characterButton.TextBlock.ForceUpperCase = true; + + jobColumnWidth = jobButton.Rect.Width; + characterColumnWidth = characterButton.Rect.Width; + } + + private void CreateSinglePlayerList(bool refresh) + { + if (refresh) + { + crew = GameMain.GameSession.CrewManager.GetCharacters(); + } + + linkedGUIList = new List(); + + for (int i = 0; i < teamIDs.Count; i++) + { + foreach (Character character in crew.Where(c => c.TeamID == teamIDs[i])) + { + CreateSinglePlayerCharacterElement(character, i); + } + } + } + + private void CreateSinglePlayerCharacterElement(Character character, int i) + { + GUIFrame frame = new GUIFrame(new RectTransform(new Point(crewListArray[i].Content.Rect.Width, GUI.IntScale(33f)), crewListArray[i].Content.RectTransform), style: "ListBoxElement") + { + UserData = character, + Color = (Character.Controlled == character) ? ownCharacterBGColor : Color.Transparent + }; + + var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: true) + { + AbsoluteSpacing = 2 + }; + + new GUICustomComponent(new RectTransform(new Point(jobColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), onDraw: (sb, component) => character.Info.DrawJobIcon(sb, component.Rect)) + { + CanBeFocused = false, + HoverColor = Color.White, + SelectedColor = Color.White + }; + + GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), + ToolBox.LimitString(character.Info.Name, GUI.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: character.Info.Job.Prefab.UIColor); + + linkedGUIList.Add(new LinkedGUI(character, frame, !character.IsDead, null)); + } + + private void CreateMultiPlayerListContentHolder(GUILayoutGroup headerFrame) + { + GUIButton jobButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("tabmenu.job"), style: "GUIButtonSmallFreeScale"); + GUIButton characterButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("name"), style: "GUIButtonSmallFreeScale"); + GUIButton pingButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("serverlistping"), style: "GUIButtonSmallFreeScale"); + + sizeMultiplier = (headerFrame.Rect.Width - headerFrame.AbsoluteSpacing * (headerFrame.CountChildren - 1)) / (float)headerFrame.Rect.Width; + + jobButton.RectTransform.RelativeSize = new Vector2(jobColumnWidthPercentage * sizeMultiplier, 1f); + characterButton.RectTransform.RelativeSize = new Vector2(characterColumnWidthPercentage * sizeMultiplier, 1f); + pingButton.RectTransform.RelativeSize = new Vector2(pingColumnWidthPercentage * sizeMultiplier, 1f); + + jobButton.TextBlock.Font = characterButton.TextBlock.Font = pingButton.TextBlock.Font = GUI.HotkeyFont; + jobButton.CanBeFocused = characterButton.CanBeFocused = pingButton.CanBeFocused = false; + jobButton.TextBlock.ForceUpperCase = characterButton.TextBlock.ForceUpperCase = pingButton.ForceUpperCase = true; + + jobColumnWidth = jobButton.Rect.Width; + characterColumnWidth = characterButton.Rect.Width; + pingColumnWidth = pingButton.Rect.Width; + } + + private void CreateMultiPlayerList(bool refresh) + { + if (refresh) + { + crew = GameMain.GameSession.CrewManager.GetCharacters(); + } + + linkedGUIList = new List(); + + List connectedClients = GameMain.Client.ConnectedClients; + + for (int i = 0; i < teamIDs.Count; i++) + { + foreach (Character character in crew.Where(c => c.TeamID == teamIDs[i])) + { + if (!(character is AICharacter) && connectedClients.Find(c => c.Character == null && c.Name == character.Name) != null) continue; + CreateMultiPlayerCharacterElement(character, GameMain.Client.ConnectedClients.Find(c => c.Character == character), i); + } + } + + for (int j = 0; j < connectedClients.Count; j++) + { + Client client = connectedClients[j]; + + if (!client.InGame || client.Character == null || client.Character.IsDead) + { + CreateMultiPlayerClientElement(client); + } + } + } + + private void CreateMultiPlayerCharacterElement(Character character, Client client, int i) + { + GUIFrame frame = new GUIFrame(new RectTransform(new Point(crewListArray[i].Content.Rect.Width, GUI.IntScale(33f)), crewListArray[i].Content.RectTransform), style: "ListBoxElement") + { + UserData = character, + Color = (GameMain.NetworkMember != null && GameMain.Client.Character == character) ? ownCharacterBGColor : Color.Transparent + }; + + var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: true) + { + AbsoluteSpacing = 2 + }; + + new GUICustomComponent(new RectTransform(new Point(jobColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), onDraw: (sb, component) => character.Info.DrawJobIcon(sb, component.Rect)) + { + CanBeFocused = false, + HoverColor = Color.White, + SelectedColor = Color.White + }; + + if (client != null) + { + CreateNameWithPermissionIcon(client, paddedFrame); + linkedGUIList.Add(new LinkedGUI(client, frame, true, new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), client.Ping.ToString(), textAlignment: Alignment.Center))); + } + else + { + GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), + ToolBox.LimitString(character.Info.Name, GUI.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: character.Info.Job.Prefab.UIColor); + + if (character is AICharacter) + { + linkedGUIList.Add(new LinkedGUI(character, frame, !character.IsDead, new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), TextManager.Get("tabmenu.bot"), textAlignment: Alignment.Center) { ForceUpperCase = true })); + } + else + { + linkedGUIList.Add(new LinkedGUI(client: null, frame, true, null)); + + new GUICustomComponent(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), onDraw: (sb, component) => DrawDisconnectedIcon(sb, component.Rect)) + { + CanBeFocused = false, + HoverColor = Color.White, + SelectedColor = Color.White + }; + } + } + } + + private void CreateMultiPlayerClientElement(Client client) + { + int teamIndex = GetTeamIndex(client); + if (teamIndex == -1) teamIndex = 0; + + GUIFrame frame = new GUIFrame(new RectTransform(new Point(crewListArray[teamIndex].Content.Rect.Width, GUI.IntScale(33f)), crewListArray[teamIndex].Content.RectTransform), style: "ListBoxElement") + { + UserData = client, + Color = Color.Transparent + }; + + var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: true) + { + AbsoluteSpacing = 2 + }; + + new GUICustomComponent(new RectTransform(new Point(jobColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), + onDraw: (sb, component) => DrawNotInGameIcon(sb, component.Rect, client)) + { + CanBeFocused = false, + HoverColor = Color.White, + SelectedColor = Color.White + }; + + CreateNameWithPermissionIcon(client, paddedFrame); + linkedGUIList.Add(new LinkedGUI(client, frame, false, new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), client.Ping.ToString(), textAlignment: Alignment.Center))); + } + + private int GetTeamIndex(Client client) + { + if (teamIDs.Count <= 1) return 0; + + if (client.Character != null) + { + return teamIDs.IndexOf(client.Character.TeamID); + } + + if (client.CharacterID != 0) + { + foreach (Character c in crew) + { + if (client.CharacterID == c.ID) + { + return teamIDs.IndexOf(c.TeamID); + } + } + } + else + { + foreach (Character c in crew) + { + if (client.Name == c.Name) + { + return teamIDs.IndexOf(c.TeamID); + } + } + } + + return 0; + } + + private void CreateNameWithPermissionIcon(Client client, GUILayoutGroup paddedFrame) + { + GUITextBlock characterNameBlock; + Sprite permissionIcon = GetPermissionIcon(client); + + if (permissionIcon != null) + { + Point iconSize = permissionIcon.SourceRect.Size; + float characterNameWidthAdjustment = (iconSize.X + paddedFrame.AbsoluteSpacing) / characterColumnWidth; + + characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), + ToolBox.LimitString(client.Name, GUI.Font, (int)(characterColumnWidth - paddedFrame.Rect.Width * characterNameWidthAdjustment)), textAlignment: Alignment.Center, textColor: client.Character != null ? client.Character.Info.Job.Prefab.UIColor : Color.White); + + float iconWidth = iconSize.X / (float)characterColumnWidth; + int xOffset = (int)(jobColumnWidth + characterNameBlock.TextPos.X - GUI.Font.MeasureString(characterNameBlock.Text).X / 2f - paddedFrame.AbsoluteSpacing - iconWidth * paddedFrame.Rect.Width); + new GUIImage(new RectTransform(new Vector2(iconWidth, 1f), paddedFrame.RectTransform) { AbsoluteOffset = new Point(xOffset + 2, 0) }, permissionIcon) { IgnoreLayoutGroups = true }; + } + else + { + characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), + ToolBox.LimitString(client.Name, GUI.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: client.Character != null ? client.Character.Info.Job.Prefab.UIColor : Color.White); + } + + if (client.Character != null && client.Character.IsDead) + { + characterNameBlock.Strikethrough = new GUITextBlock.StrikethroughSettings(null, GUI.IntScale(1f), GUI.IntScale(5f)); + } + } + + private Sprite GetPermissionIcon(Client client) + { + if (GameMain.NetworkMember == null || client == null || !client.HasPermissions) return null; + + if (!client.AllowKicking) // Owner cannot be kicked + { + return ownerIcon; + } + else + { + return moderatorIcon; + } + } + + private void DrawNotInGameIcon(SpriteBatch spriteBatch, Rectangle area, Client client) + { + if (client.Spectating) + { + spectateIcon.Draw(spriteBatch, area, Color.White); + } + else if (client.Character != null && client.Character.IsDead) + { + client.Character.Info.DrawJobIcon(spriteBatch, area); + } + else + { + Vector2 stringOffset = GUI.GlobalFont.MeasureString(inLobbyString) / 2f; + GUI.GlobalFont.DrawString(spriteBatch, inLobbyString, area.Center.ToVector2() - stringOffset, Color.White); + } + } + + private void DrawDisconnectedIcon(SpriteBatch spriteBatch, Rectangle area) + { + disconnectedIcon.Draw(spriteBatch, area, GUI.Style.Red); + } + + /// + /// Select an element from CrewListFrame + /// + private bool SelectElement(object userData, GUIComponent crewList) + { + Character character = userData as Character; + Client client = userData as Client; + + GUIComponent existingPreview = infoFrameHolder.FindChild("SelectedCharacter"); + if (existingPreview != null) infoFrameHolder.RemoveChild(existingPreview); + + GUIFrame background = new GUIFrame(new RectTransform(new Vector2(0.543f, 0.717f), infoFrameHolder.RectTransform, Anchor.TopLeft, Pivot.TopRight) { RelativeOffset = new Vector2(-0.061f, 0) }) + { + UserData = "SelectedCharacter" + }; + + if (character != null) + { + if (GameMain.NetworkMember == null) + { + GUIComponent preview = character.Info.CreateInfoFrame(background, false, null); + } + else + { + GUIComponent preview = character.Info.CreateInfoFrame(background, false, GetPermissionIcon(GameMain.Client.ConnectedClients.Find(c => c.Character == character))); + GameMain.Client.SelectCrewCharacter(character, preview); + } + } + else if (client != null) + { + GUIComponent preview = CreateClientInfoFrame(background, client, GetPermissionIcon(client)); + if (GameMain.NetworkMember != null) GameMain.Client.SelectCrewClient(client, preview); + } + + return true; + } + + private GUIComponent CreateClientInfoFrame(GUIFrame frame, Client client, Sprite permissionIcon = null) + { + GUIComponent paddedFrame; + + if (client.Character == null) + { + paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.874f, 0.58f), frame.RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.05f) }) + { + RelativeSpacing = 0.05f + //Stretch = true + }; + + var headerArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.322f), paddedFrame.RectTransform), isHorizontal: true); + + new GUICustomComponent(new RectTransform(new Vector2(0.425f, 1.0f), headerArea.RectTransform), + onDraw: (sb, component) => DrawNotInGameIcon(sb, component.Rect, client)); + + ScalableFont font = paddedFrame.Rect.Width < 280 ? GUI.SmallFont : GUI.Font; + + var headerTextArea = new GUILayoutGroup(new RectTransform(new Vector2(0.575f, 1.0f), headerArea.RectTransform)) + { + RelativeSpacing = 0.02f, + Stretch = true + }; + + GUITextBlock clientNameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), ToolBox.LimitString(client.Name, GUI.Font, headerTextArea.Rect.Width), textColor: Color.White, font: GUI.Font) + { + ForceUpperCase = true, + Padding = Vector4.Zero + }; + + if (permissionIcon != null) + { + Point iconSize = permissionIcon.SourceRect.Size; + int iconWidth = (int)((float)clientNameBlock.Rect.Height / iconSize.Y * iconSize.X); + new GUIImage(new RectTransform(new Point(iconWidth, clientNameBlock.Rect.Height), clientNameBlock.RectTransform) { AbsoluteOffset = new Point(-iconWidth - 2, 0) }, permissionIcon) { IgnoreLayoutGroups = true }; + } + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), client.Spectating ? TextManager.Get("playingasspectator") : TextManager.Get("tabmenu.inlobby"), textColor: Color.White, font: font, wrap: true) + { + Padding = Vector4.Zero + }; + } + else + { + paddedFrame = client.Character.Info.CreateInfoFrame(frame, false, permissionIcon); + } + + return paddedFrame; + } + + private void CreateMultiPlayerLogContent(GUIFrame crewFrame) + { + var logContainer = new GUIFrame(new RectTransform(new Vector2(0.543f, 0.717f), crewFrame.RectTransform, Anchor.TopRight, Pivot.TopLeft) { RelativeOffset = new Vector2(-0.061f, 0) }); + var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.900f, 0.900f), logContainer.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { RelativeOffset = new Vector2(0f, 0.0475f) }, style: null); + var content = new GUILayoutGroup(new RectTransform(Vector2.One, innerFrame.RectTransform)) + { + Stretch = true + }; + + logList = new GUIListBox(new RectTransform(Vector2.One, content.RectTransform)) + { + Padding = new Vector4(0, 10 * GUI.Scale, 0, 10 * GUI.Scale), + UserData = crewFrame, + AutoHideScrollBar = false, + Spacing = (int)(5 * GUI.Scale) + }; + + foreach (Pair pair in storedMessages) + { + AddLineToLog(pair.First, pair.Second); + } + + logList.BarScroll = 1f; + } + + private static readonly List> storedMessages = new List>(); + + public static void StorePlayerConnectionChangeMessage(ChatMessage message) + { + if (!GameMain.GameSession?.GameMode?.IsRunning ?? true) { return; } + + string msg = ChatMessage.GetTimeStamp() + message.TextWithSender; + storedMessages.Add(new Pair(msg, message.ChangeType)); + + if (GameSession.IsTabMenuOpen) + { + TabMenu instance = GameSession.TabMenuInstance; + instance.AddLineToLog(msg, message.ChangeType); + + // Update crew + if (selectedTab == InfoFrameTab.Crew) + { + instance.RemoveCurrentElements(); + instance.CreateMultiPlayerList(true); + } + } + } + + private void RemoveCurrentElements() + { + for (int i = 0; i < crewListArray.Length; i++) + { + for (int j = 0; j < linkedGUIList.Count; j++) + { + linkedGUIList[j].Remove(crewListArray[i].Content); + } + } + + linkedGUIList.Clear(); + } + + private void AddLineToLog(string line, PlayerConnectionChangeType type) + { + Color textColor = Color.White; + + switch (type) + { + case PlayerConnectionChangeType.Joined: + textColor = GUI.Style.Green; + break; + case PlayerConnectionChangeType.Kicked: + textColor = GUI.Style.Orange; + break; + case PlayerConnectionChangeType.Disconnected: + textColor = GUI.Style.Yellow; + break; + case PlayerConnectionChangeType.Banned: + textColor = GUI.Style.Red; + break; + } + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), logList.Content.RectTransform), line, wrap: true, font: GUI.SmallFont) + { + TextColor = textColor, + CanBeFocused = false, + UserData = line + }.CalculateHeightFromText(); + + //if ((prevSize == 1.0f && listBox.BarScroll == 0.0f) || (prevSize < 1.0f && listBox.BarScroll == 1.0f)) listBox.BarScroll = 1.0f; + } + + private void CreateMissionInfo(GUIFrame infoFrame) + { + infoFrame.ClearChildren(); + GUIFrame missionFrame = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox"); + int padding = (int)(0.0245f * missionFrame.Rect.Height); + Location endLocation = GameMain.GameSession.EndLocation; + Sprite portrait = endLocation.Type.GetPortrait(endLocation.PortraitId); + bool hasPortrait = portrait != null && portrait.SourceRect.Width > 0 && portrait.SourceRect.Height > 0; + int contentWidth = hasPortrait ? (int)(missionFrame.Rect.Width * 0.951f) : missionFrame.Rect.Width - padding * 2; + + Vector2 locationNameSize = GUI.LargeFont.MeasureString(endLocation.Name); + Vector2 locationTypeSize = GUI.SubHeadingFont.MeasureString(endLocation.Name); + GUITextBlock locationNameText = new GUITextBlock(new RectTransform(new Point(contentWidth, (int)locationNameSize.Y), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, padding) }, endLocation.Name, font: GUI.LargeFont); + GUITextBlock locationTypeText = new GUITextBlock(new RectTransform(new Point(contentWidth, (int)locationTypeSize.Y), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, locationNameText.Rect.Height + padding) }, endLocation.Type.Name, font: GUI.SubHeadingFont); + + int locationInfoYOffset = locationNameText.Rect.Height + locationTypeText.Rect.Height + padding * 2; + + GUIFrame missionDescriptionHolder; + + if (hasPortrait) + { + GUIFrame missionImageHolder = new GUIFrame(new RectTransform(new Point(contentWidth, (int)(missionFrame.Rect.Height * 0.588f)), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, locationInfoYOffset) }); + float portraitAspectRatio = portrait.SourceRect.Width / portrait.SourceRect.Height; + GUIImage portraitImage = new GUIImage(new RectTransform(new Vector2(1.0f, 1f), missionImageHolder.RectTransform), portrait, scaleToFit: true); + missionImageHolder.RectTransform.NonScaledSize = new Point(portraitImage.Rect.Size.X, (int)(portraitImage.Rect.Size.X / portraitAspectRatio)); + missionDescriptionHolder = new GUIFrame(new RectTransform(new Point(contentWidth, 0), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, missionImageHolder.RectTransform.AbsoluteOffset.Y + missionImageHolder.Rect.Height + padding) }, style: null); + } + else + { + missionDescriptionHolder = new GUIFrame(new RectTransform(new Point(contentWidth, 0), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, locationInfoYOffset) }, style: null); + } + + Mission mission = GameMain.GameSession?.Mission; + if (mission != null) + { + GUILayoutGroup missionTextGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.744f, 0f), missionDescriptionHolder.RectTransform, Anchor.CenterLeft) { RelativeOffset = new Vector2(0.225f, 0f) }, false, childAnchor: Anchor.TopLeft); + + string missionNameString = ToolBox.WrapText(mission.Name, missionTextGroup.Rect.Width, GUI.LargeFont); + string missionDescriptionString = ToolBox.WrapText(mission.Description, missionTextGroup.Rect.Width, GUI.Font); + string missionRewardString = ToolBox.WrapText(TextManager.GetWithVariable("MissionReward", "[reward]", mission.Reward.ToString()), missionTextGroup.Rect.Width, GUI.Font); + + Vector2 missionNameSize = GUI.LargeFont.MeasureString(missionNameString); + Vector2 missionDescriptionSize = GUI.Font.MeasureString(missionDescriptionString); + Vector2 missionRewardSize = GUI.Font.MeasureString(missionRewardString); + + missionDescriptionHolder.RectTransform.NonScaledSize = new Point(missionDescriptionHolder.RectTransform.NonScaledSize.X, (int)(missionNameSize.Y + missionDescriptionSize.Y + missionRewardSize.Y)); + missionTextGroup.RectTransform.NonScaledSize = new Point(missionTextGroup.RectTransform.NonScaledSize.X, missionDescriptionHolder.RectTransform.NonScaledSize.Y); + + float iconAspectRatio = mission.Prefab.Icon.SourceRect.Width / mission.Prefab.Icon.SourceRect.Height; + int iconWidth = (int)(0.225f * missionDescriptionHolder.RectTransform.NonScaledSize.X); + int iconHeight = Math.Max(missionTextGroup.RectTransform.NonScaledSize.Y, (int)(iconWidth * iconAspectRatio)); + Point iconSize = new Point(iconWidth, iconHeight); + + new GUIImage(new RectTransform(iconSize, missionDescriptionHolder.RectTransform), mission.Prefab.Icon, null, true) { Color = mission.Prefab.IconColor }; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionNameString, font: GUI.LargeFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionRewardString); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionDescriptionString); + } + else + { + GUILayoutGroup missionTextGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0f), missionDescriptionHolder.RectTransform, Anchor.CenterLeft), false, childAnchor: Anchor.TopLeft); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), TextManager.Get("NoMission"), font: GUI.LargeFont); + } + } + + private void CreateTraitorInfo(GUIFrame infoFrame, TraitorMissionPrefab traitorMission, Character traitor) + { + GUIFrame missionFrame = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox"); + + int padding = (int)(0.0245f * missionFrame.Rect.Height); + + GUIFrame missionDescriptionHolder = new GUIFrame(new RectTransform(new Point(missionFrame.Rect.Width - padding * 2, 0), missionFrame.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, padding) }, style: null); + GUILayoutGroup missionTextGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.65f, 0f), missionDescriptionHolder.RectTransform, Anchor.CenterLeft) { RelativeOffset = new Vector2(0.319f, 0f) }, false, childAnchor: Anchor.TopLeft); + + string missionNameString = ToolBox.WrapText(TextManager.Get("tabmenu.traitor"), missionTextGroup.Rect.Width, GUI.LargeFont); + string missionDescriptionString = ToolBox.WrapText(traitor.TraitorCurrentObjective, missionTextGroup.Rect.Width, GUI.Font); + + Vector2 missionNameSize = GUI.LargeFont.MeasureString(missionNameString); + Vector2 missionDescriptionSize = GUI.Font.MeasureString(missionDescriptionString); + + missionDescriptionHolder.RectTransform.NonScaledSize = new Point(missionDescriptionHolder.RectTransform.NonScaledSize.X, (int)(missionNameSize.Y + missionDescriptionSize.Y)); + missionTextGroup.RectTransform.NonScaledSize = new Point(missionTextGroup.RectTransform.NonScaledSize.X, missionDescriptionHolder.RectTransform.NonScaledSize.Y); + + float aspectRatio = traitorMission.Icon.SourceRect.Width / traitorMission.Icon.SourceRect.Height; + + int iconWidth = (int)(0.319f * missionDescriptionHolder.RectTransform.NonScaledSize.X); + int iconHeight = Math.Max(missionTextGroup.RectTransform.NonScaledSize.Y, (int)(iconWidth * aspectRatio)); + Point iconSize = new Point(iconWidth, iconHeight); + + new GUIImage(new RectTransform(iconSize, missionDescriptionHolder.RectTransform), traitorMission.Icon, null, true) { Color = traitorMission.IconColor }; + + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionNameString, font: GUI.LargeFont); + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), missionDescriptionString); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 136f5e544..2224d5cfc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -66,7 +66,7 @@ namespace Barotrauma if (vanillaContent == null) { // TODO: Dynamic method for defining and finding the vanilla content package. - vanillaContent = ContentPackage.List.SingleOrDefault(cp => Path.GetFileName(cp.Path).ToLowerInvariant() == "vanilla 0.9.xml"); + vanillaContent = ContentPackage.List.SingleOrDefault(cp => Path.GetFileName(cp.Path).Equals("vanilla 0.9.xml", StringComparison.OrdinalIgnoreCase)); } return vanillaContent; } @@ -112,6 +112,8 @@ namespace Barotrauma public event Action OnResolutionChanged; + private bool exiting; + public static GameMain Instance { get; @@ -144,7 +146,17 @@ namespace Barotrauma public static bool WindowActive { - get { return Instance == null || Instance.IsActive; } + get + { + try + { + return Instance != null && !Instance.exiting && Instance.IsActive; + } + catch (NullReferenceException) + { + return false; + } + } } public static GameClient Client; @@ -179,10 +191,14 @@ namespace Barotrauma { Content.RootDirectory = "Content"; - GraphicsDeviceManager = new GraphicsDeviceManager(this); - - GraphicsDeviceManager.IsFullScreen = false; - GraphicsDeviceManager.GraphicsProfile = GfxProfile; +#if DEBUG && WINDOWS + GraphicsAdapter.UseDebugLayers = true; +#endif + GraphicsDeviceManager = new GraphicsDeviceManager(this) + { + IsFullScreen = false, + GraphicsProfile = GfxProfile + }; GraphicsDeviceManager.ApplyChanges(); Window.Title = "Barotrauma"; @@ -329,6 +345,8 @@ namespace Barotrauma //do this here because we need it for the loading screen WaterRenderer.Instance = new WaterRenderer(base.GraphicsDevice, Content); + Quad.Init(GraphicsDevice); + loadingScreenOpen = true; TitleScreen = new LoadingScreen(GraphicsDevice) { @@ -385,6 +403,13 @@ namespace Barotrauma } } + public class LoadingException : Exception + { + public LoadingException(Exception e) : base("Loading was interrupted due to an error.", innerException: e) + { + } + } + private IEnumerable Load(bool isSeparateThread) { if (GameSettings.VerboseLogging) @@ -402,7 +427,7 @@ namespace Barotrauma SoundManager.SetCategoryGainMultiplier("ui", Config.SoundVolume, 0); SoundManager.SetCategoryGainMultiplier("waterambience", Config.SoundVolume, 0); SoundManager.SetCategoryGainMultiplier("music", Config.MusicVolume, 0); - SoundManager.SetCategoryGainMultiplier("voip", Config.VoiceChatVolume * 20.0f, 0); + SoundManager.SetCategoryGainMultiplier("voip", Math.Min(Config.VoiceChatVolume, 1.0f), 0); if (Config.EnableSplashScreen && !ConsoleArguments.Contains("-skipintro")) { @@ -430,7 +455,7 @@ namespace Barotrauma { bool waitingForWorkshopUpdates = true; bool result = false; - TaskPool.Add(SteamManager.AutoUpdateWorkshopItems(), (task) => + TaskPool.Add(SteamManager.AutoUpdateWorkshopItemsAsync(), (task) => { result = task.Result; waitingForWorkshopUpdates = false; @@ -503,6 +528,7 @@ namespace Barotrauma Tutorials.Tutorial.Init(); MapGenerationParams.Init(); LevelGenerationParams.LoadPresets(); + WreckAIConfig.LoadAll(); ScriptedEventSet.LoadPrefabs(); AfflictionPrefab.LoadAll(GetFilesOfType(ContentType.Afflictions)); SkillSettings.Load(GetFilesOfType(ContentType.SkillSettings)); @@ -518,6 +544,7 @@ namespace Barotrauma yield return CoroutineStatus.Running; JobPrefab.LoadAll(GetFilesOfType(ContentType.Jobs)); + CorpsePrefab.LoadAll(GetFilesOfType(ContentType.Corpses)); NPCConversation.LoadAll(GetFilesOfType(ContentType.NPCConversations)); @@ -527,7 +554,7 @@ namespace Barotrauma GameModePreset.Init(); - Submarine.RefreshSavedSubs(); + SubmarineInfo.RefreshSavedSubs(); TitleScreen.LoadState = 65.0f; yield return CoroutineStatus.Running; @@ -664,6 +691,8 @@ namespace Barotrauma { #if DEBUG DebugConsole.ThrowError($"Failed to parse a Steam friend's connect invitation command ({connectCommand})", e); +#else + DebugConsole.Log($"Failed to parse a Steam friend's connect invitation command ({connectCommand})\n" + e.StackTrace); #endif ConnectName = null; ConnectEndpoint = null; @@ -756,12 +785,7 @@ namespace Barotrauma if (!hasLoaded && !CoroutineManager.IsCoroutineRunning(loadingCoroutine)) { - string errMsg = "Loading was interrupted due to an error"; - if (loadingCoroutine.Exception != null) - { - errMsg += ": " + loadingCoroutine.Exception.Message + "\n" + loadingCoroutine.Exception.StackTrace; - } - throw new Exception(errMsg); + throw new LoadingException(loadingCoroutine.Exception); } } else if (hasLoaded) @@ -823,6 +847,10 @@ namespace Barotrauma { (GameSession.GameMode as TutorialMode).Tutorial.CloseActiveContentGUI(); } + else if (GameSession.IsTabMenuOpen) + { + gameSession.ToggleTabMenu(); + } else if (GUI.PauseMenuOpen) { GUI.TogglePauseMenu(); @@ -831,7 +859,8 @@ namespace Barotrauma else if ((Character.Controlled == null || !itemHudActive()) //TODO: do we need to check Inventory.SelectedSlot? && Inventory.SelectedSlot == null && CharacterHealth.OpenHealthWindow == null - && !CrewManager.IsCommandInterfaceOpen) + && !CrewManager.IsCommandInterfaceOpen + && !(Screen.Selected is SubEditorScreen editor && !editor.WiringMode && Character.Controlled.SelectedConstruction != null)) { // Otherwise toggle pausing, unless another window/interface is open. GUI.TogglePauseMenu(); @@ -922,7 +951,7 @@ namespace Barotrauma sw.Stop(); PerformanceCounter.AddElapsedTicks("Update total", sw.ElapsedTicks); - PerformanceCounter.UpdateTimeGraph.Update(sw.ElapsedTicks / (float)TimeSpan.TicksPerMillisecond); + PerformanceCounter.UpdateTimeGraph.Update(sw.ElapsedTicks * 1000.0f / (float)Stopwatch.Frequency); PerformanceCounter.UpdateIterationsGraph.Update(updateIterations); } @@ -944,10 +973,13 @@ namespace Barotrauma double deltaTime = gameTime.ElapsedGameTime.TotalSeconds; - double step = 1.0 / Timing.FrameLimit; - while (!Config.VSyncEnabled && sw.Elapsed.TotalSeconds + deltaTime < step) + if (Timing.FrameLimit > 0) { - Thread.Sleep(1); + double step = 1.0 / Timing.FrameLimit; + while (!Config.VSyncEnabled && sw.Elapsed.TotalSeconds + deltaTime < step) + { + Thread.Sleep(1); + } } PerformanceCounter.Update(sw.Elapsed.TotalSeconds + deltaTime); @@ -972,7 +1004,7 @@ namespace Barotrauma sw.Stop(); PerformanceCounter.AddElapsedTicks("Draw total", sw.ElapsedTicks); - PerformanceCounter.DrawTimeGraph.Update(sw.ElapsedTicks / (float)TimeSpan.TicksPerMillisecond); + PerformanceCounter.DrawTimeGraph.Update(sw.ElapsedTicks * 1000.0f / (float)Stopwatch.Frequency); } @@ -1101,7 +1133,10 @@ namespace Barotrauma UserData = "https://steamcommunity.com/app/602960/discussions/1/", OnClicked = (btn, userdata) => { - SteamManager.OverlayCustomURL(userdata as string); + if (!SteamManager.OverlayCustomURL(userdata as string)) + { + ShowOpenUrlInWebBrowserPrompt(userdata as string); + } msgBox.Close(); return true; } @@ -1138,7 +1173,9 @@ namespace Barotrauma protected override void OnExiting(object sender, EventArgs args) { - if (NetworkMember != null) NetworkMember.Disconnect(); + exiting = true; + DebugConsole.NewMessage("Exiting..."); + NetworkMember?.Disconnect(); SteamManager.ShutDown(); try diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs index b9bdbc7de..4ad42edb8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/CrewManager.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using Barotrauma.Steam; namespace Barotrauma { @@ -34,6 +35,10 @@ namespace Barotrauma private GUIButton commandButton, toggleCrewButton; private float crewListOpenState; private bool toggleCrewListOpen = true; + private Point crewListEntrySize; + + private GUIFrame contextMenu; + private GUIListBox subContextMenu; /// /// Present only in single player games. In multiplayer. The chatbox is found from GameSession.Client. @@ -51,7 +56,6 @@ namespace Barotrauma { if (toggleCrewListOpen == value) { return; } toggleCrewListOpen = GameMain.Config.CrewMenuOpen = value; - toggleCrewButton.Children.ForEach(c => c.SpriteEffects = toggleCrewListOpen ? SpriteEffects.None : SpriteEffects.FlipHorizontally); } } @@ -70,13 +74,13 @@ namespace Barotrauma { foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "character") continue; + if (!subElement.Name.ToString().Equals("character", StringComparison.OrdinalIgnoreCase)) { continue; } var characterInfo = new CharacterInfo(subElement); characterInfos.Add(characterInfo); foreach (XElement invElement in subElement.Elements()) { - if (invElement.Name.ToString().ToLowerInvariant() != "inventory") continue; + if (!invElement.Name.ToString().Equals("inventory", StringComparison.OrdinalIgnoreCase)) { continue; } characterInfo.InventoryData = invElement; break; } @@ -90,6 +94,8 @@ namespace Barotrauma CanBeFocused = false }; + #region Crew Area + var crewAreaWithButtons = new GUIFrame( HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.CrewArea, guiFrame.RectTransform), style: null, @@ -98,11 +104,13 @@ namespace Barotrauma CanBeFocused = false }; - var buttonHeight = (int)(GUI.Scale * 40); + var commandButtonHeight = (int)(GUI.Scale * 40); + var buttonSize = new Point((int)(182f / 99f * commandButtonHeight), commandButtonHeight); + var crewListToggleButtonHeight = (int)(64f * buttonSize.X / 175f); crewArea = new GUIFrame( new RectTransform( - new Point(crewAreaWithButtons.Rect.Width, crewAreaWithButtons.Rect.Height - (int)(1.5f * buttonHeight) - 2 * HUDLayoutSettings.Padding), + new Point(crewAreaWithButtons.Rect.Width, crewAreaWithButtons.Rect.Height - commandButtonHeight - crewListToggleButtonHeight - 2 * HUDLayoutSettings.Padding), crewAreaWithButtons.RectTransform, Anchor.BottomLeft), style: null, @@ -111,7 +119,6 @@ namespace Barotrauma CanBeFocused = false }; - var buttonSize = new Point((int)(182.0f / 99.0f * buttonHeight), buttonHeight); commandButton = new GUIButton( new RectTransform(buttonSize, parent: crewAreaWithButtons.RectTransform), style: "CommandButton") @@ -139,14 +146,13 @@ namespace Barotrauma Spacing = (int)(GUI.Scale * 10) }; + buttonSize.Y = crewListToggleButtonHeight; toggleCrewButton = new GUIButton( - new RectTransform( - new Point(buttonSize.X, (int)(0.5f * buttonHeight)), - parent: crewAreaWithButtons.RectTransform) + new RectTransform(buttonSize, parent: crewAreaWithButtons.RectTransform) { - AbsoluteOffset = new Point(0, buttonHeight + HUDLayoutSettings.Padding) + AbsoluteOffset = new Point(0, commandButtonHeight + HUDLayoutSettings.Padding) }, - style: "UIToggleButton") + style: "CrewListToggleButton") { OnClicked = (GUIButton btn, object userdata) => { @@ -159,6 +165,17 @@ namespace Barotrauma previousOrderArrow = new Sprite("Content/UI/CommandUIAtlas.png", new Rectangle(128, 512, 128, 128)); cancelIcon = new Sprite("Content/UI/CommandUIAtlas.png", new Rectangle(512, 384, 128, 128)); + // Calculate and store crew list entry size so it doesn't have to be calculated for every entry + crewListEntrySize = new Point(crewList.Content.Rect.Width - HUDLayoutSettings.Padding, 0); + int crewListEntryMinHeight = 32; + crewListEntrySize.Y = Math.Max(crewListEntryMinHeight, (int)(crewListEntrySize.X / 8f)); + float charactersPerView = crewList.Content.Rect.Height / (float)(crewListEntrySize.Y + crewList.Spacing); + int adjustedHeight = (int)Math.Ceiling(crewList.Content.Rect.Height / Math.Round(charactersPerView)) - crewList.Spacing; + if (adjustedHeight < crewListEntryMinHeight) { adjustedHeight = (int)Math.Ceiling(crewList.Content.Rect.Height / Math.Floor(charactersPerView)) - crewList.Spacing; } + crewListEntrySize.Y = adjustedHeight; + + #endregion + #region Chatbox if (IsSinglePlayer) @@ -212,7 +229,7 @@ namespace Barotrauma var chatBox = ChatBox ?? GameMain.Client?.ChatBox; if (chatBox != null) { - chatBox.ToggleButton = new GUIButton(new RectTransform(new Point((int)(182f * GUI.Scale * 0.4f), (int)(99f * GUI.Scale * 0.4f)), guiFrame.RectTransform), style: "ChatToggleButton"); + chatBox.ToggleButton = new GUIButton(new RectTransform(new Point((int)(182f * GUI.Scale * 0.4f), (int)(99f * GUI.Scale * 0.4f)), chatBox.GUIFrame.Parent.RectTransform), style: "ChatToggleButton"); chatBox.ToggleButton.RectTransform.AbsoluteOffset = new Point(0, HUDLayoutSettings.ChatBoxArea.Height - chatBox.ToggleButton.Rect.Height); chatBox.ToggleButton.OnClicked += (GUIButton btn, object userdata) => { @@ -248,6 +265,8 @@ namespace Barotrauma OnClicked = (GUIButton button, object userData) => { if (!CanIssueOrders) { return false; } + var sub = Character.Controlled.Submarine; + if (sub == null || sub.TeamID != Character.Controlled.TeamID || sub.Info.IsWreck) { return false; } SetCharacterOrder(null, order, null, Character.Controlled); var visibleHulls = new List(Character.Controlled.GetVisibleHulls()); foreach (var hull in visibleHulls) @@ -330,6 +349,7 @@ namespace Barotrauma } AddCharacterToCrewList(character); + DisplayCharacterOrder(character, character.CurrentOrder, character.CurrentOrderOption); } public void AddCharacterInfo(CharacterInfo characterInfo) @@ -372,16 +392,26 @@ namespace Barotrauma { if (character == null) { return; } - int width = crewList.Content.Rect.Width - HUDLayoutSettings.Padding; - int height = Math.Max(32, (int)((1.0f / 8.0f) * width)); var background = new GUIFrame( - new RectTransform(new Point(width, height), parent: crewList.Content.RectTransform, anchor: Anchor.TopRight), + new RectTransform(crewListEntrySize, parent: crewList.Content.RectTransform, anchor: Anchor.TopRight), style: "CrewListBackground") { - UserData = character + UserData = character, + OnSecondaryClicked = (comp, data) => + { + if (data == null) { return false; } + + var client = GameMain.NetworkMember?.ConnectedClients?.Find(c => c.Character == data); + if (client != null) + { + CreateModerationContextMenu(PlayerInput.MousePosition.ToPoint(), client); + return true; + } + return false; + } }; - var iconRelativeWidth = (float)height / background.Rect.Width; + var iconRelativeWidth = (float)crewListEntrySize.Y / background.Rect.Width; var layoutGroup = new GUILayoutGroup( new RectTransform(Vector2.One, parent: background.RectTransform), @@ -455,7 +485,7 @@ namespace Barotrauma characterButton.ToolTip = characterTooltip; if (character.Info?.Job?.Prefab != null) { - characterButton.TooltipColorData = new List() { new ColorData() + characterButton.TooltipRichTextData = new List() { new RichTextData() { Color = character.Info.Job.Prefab.UIColor, EndIndex = characterTooltip.Length - 1 @@ -665,6 +695,8 @@ namespace Barotrauma #endregion + #region Crew List Order Displayment + /// /// Sets the character's current order (if it's close enough to receive messages from orderGiver) and /// displays the order in the crew UI @@ -675,7 +707,7 @@ namespace Barotrauma { if (orderGiver == null || orderGiver.CurrentHull == null) { return; } var hull = orderGiver.CurrentHull; - AddOrder(new Order(order.Prefab, hull, null, orderGiver), order.Prefab.FadeOutTime); + AddOrder(new Order(order.Prefab ?? order, hull, null, orderGiver), order.FadeOutTime); if (IsSinglePlayer) { orderGiver.Speak( @@ -689,11 +721,8 @@ namespace Barotrauma } else { - if (character == null) - { - //can't issue an order if no characters are available - return; - } + //can't issue an order if no characters are available + if (character == null) { return; } if (IsSinglePlayer) { @@ -703,7 +732,7 @@ namespace Barotrauma } else if (orderGiver != null) { - OrderChatMessage msg = new OrderChatMessage(order, option, order?.TargetItemComponent?.Item, character, orderGiver); + OrderChatMessage msg = new OrderChatMessage(order, option, order?.TargetEntity ?? order?.TargetItemComponent?.Item, character, orderGiver); GameMain.Client?.SendChatMessage(msg); } } @@ -853,6 +882,8 @@ namespace Barotrauma } } + #endregion + #region Updating and drawing the UI private void DrawMiniMapOverlay(SpriteBatch spriteBatch, GUICustomComponent container) @@ -881,6 +912,213 @@ namespace Barotrauma GUI.DrawLine(spriteBatch, center + start, center + end, Color.DarkCyan * Rand.Range(0.3f, 0.35f), width: 10); } } + + #region Context Menu + + public void CreateModerationContextMenu(Point mousePos, Client client) + { + if (IsSinglePlayer || client == null || (GameMain.NetworkMember?.ConnectedClients?.All(match => match != client) ?? true)) { return; } + + contextMenu = new GUIFrame(new RectTransform(new Vector2(0.1f, 0.12f), GUI.Canvas) { ScreenSpaceOffset = mousePos }, style: "GUIToolTip") { UserData = client }; + + var nameLabel = new GUITextBlock(new RectTransform(new Vector2(1f, 0.2f), contextMenu.RectTransform), client.Name, font: GUI.SubHeadingFont) + { + Padding = new Vector4(8), + TextColor = client.Character?.Info?.Job.Prefab.UIColor ?? Color.White + }; + + var optionsList = new GUIListBox(new RectTransform(new Vector2(1f, 0.8f), contextMenu.RectTransform, Anchor.BottomLeft), style: null) + { + Padding = new Vector4(4, 0, 4, 4) + }; + + bool hasSteam = client.SteamID > 0 && SteamManager.IsInitialized, + canKick = GameMain.Client.HasPermission(ClientPermissions.Kick), + canBan = GameMain.Client.HasPermission(ClientPermissions.Ban) && client.AllowKicking, + canPromo = GameMain.Client.HasPermission(ClientPermissions.ManagePermissions); + + // Disable options if we are targeting ourselves + if (client.ID == GameMain.Client?.ID) + { + canKick = canBan = canPromo = false; + } + + RectTransform parent = optionsList.Content.RectTransform; + new GUITextBlock(new RectTransform(Point.Zero, parent), TextManager.Get("viewsteamprofile"), font: GUI.SmallFont) + { + Padding = new Vector4(4), + Enabled = hasSteam, + UserData = "steam" + }; + + new GUITextBlock(new RectTransform(Point.Zero, parent), TextManager.Get("permissions"), font: GUI.SmallFont) + { + Padding = new Vector4(4), + Enabled = canPromo, + UserData = "promote" + }; + + new GUITextBlock(new RectTransform(Point.Zero, parent), TextManager.Get(client.MutedLocally ? "unmute" : "mute"), font: GUI.SmallFont) + { + Padding = new Vector4(4), + Enabled = client.ID != GameMain.Client?.ID, + UserData = "mute" + }; + + new GUITextBlock(new RectTransform(Point.Zero, parent), TextManager.Get(canKick ? "kick" : "votetokick"), font: GUI.SmallFont) + { + Padding = new Vector4(4), + Enabled = client.ID != GameMain.Client?.ID && client.AllowKicking, + UserData = canKick ? "kick" : "votekick" + }; + + new GUITextBlock(new RectTransform(Point.Zero, parent), TextManager.Get("ban"), font: GUI.SmallFont) + { + Padding = new Vector4(4), + Enabled = canBan, + UserData = "ban" + }; + + foreach (GUIComponent c in optionsList.Content.Children) + { + if (c is GUITextBlock child && !child.Enabled) + { + child.TextColor *= 0.5f; + } + } + + var children = optionsList.Content.Children.ToList(); + + // Resize all children to the size of their text + foreach (GUITextBlock block in children.Where(c => c is GUITextBlock).Cast()) + { + block.RectTransform.NonScaledSize = new Point((int) (block.TextSize.X + (block.Padding.X + block.Padding.Z)), (int)(18 * GUI.Scale)); + } + + int horizontalPadding = (int)(optionsList.Padding.X + optionsList.Padding.Z); + int verticalPadding = (int)(optionsList.Padding.Y + optionsList.Padding.W); + int largestWidth = children.Max(c => c.Rect.Width + horizontalPadding); + + // If the name is bigger than any of the options then overwrite + nameLabel.RectTransform.MinSize = new Point((int)(nameLabel.TextSize.X + (nameLabel.Padding.X + nameLabel.Padding.Z)), nameLabel.RectTransform.NonScaledSize.Y); + if (largestWidth < nameLabel.RectTransform.MinSize.X) { largestWidth = nameLabel.RectTransform.MinSize.X; } + + // Resize all children to the size of the longest element + foreach (GUIComponent c in children) { c.RectTransform.MinSize = new Point(largestWidth, c.Rect.Height); } + + // crop the context menu + contextMenu.RectTransform.NonScaledSize = new Point(largestWidth, (children.Sum(c => c.Rect.Height) + verticalPadding) + nameLabel.Rect.Height); + + // if the menu would go off the screen then move it up + if (contextMenu.Rect.Bottom > GameMain.GraphicsHeight) + { + contextMenu.RectTransform.ScreenSpaceOffset = new Point(mousePos.X, mousePos.Y - contextMenu.Rect.Height); + } + + optionsList.OnSelected = (component, obj) => + { + if (component.Enabled) + { + switch (obj) + { + case "steam": + Steamworks.SteamFriends.OpenWebOverlay($"https://steamcommunity.com/profiles/{client.SteamID}"); + break; + case "mute": + client.MutedLocally = !client.MutedLocally; + break; + case "kick": + GameMain.Client?.CreateKickReasonPrompt(client.Name, false); + break; + case "votekick": + GameMain.Client?.VoteForKick(client); + break; + case "ban": + GameMain.Client?.CreateKickReasonPrompt(client.Name, true); + break; + } + contextMenu = null; + return true; + } + return false; + }; + } + + private void CreatePromoteSubMenu(Point pos, Client client) + { + if (client == null ) { return; } + + subContextMenu = new GUIListBox(new RectTransform(new Vector2(0.1f, 0.1f), GUI.Canvas) { ScreenSpaceOffset = pos }, style: "GUIToolTip"); + + foreach (var rank in PermissionPreset.List) + { + new GUITextBlock(new RectTransform(Point.Zero, subContextMenu.Content.RectTransform), rank.Name, font: GUI.SmallFont) + { + ToolTip = rank.Description, + UserData = rank, + Padding = new Vector4(4) + }; + } + + var children = subContextMenu.Content.Children.ToList(); + + // Resize all children to the size of their text + foreach (GUITextBlock block in children.Where(c => c is GUITextBlock).Cast()) + { + block.RectTransform.NonScaledSize = new Point((int) (block.TextSize.X + (block.Padding.X + block.Padding.Z)), (int)(18 * GUI.Scale)); + } + + int horizontalPadding = (int)(subContextMenu.Padding.X + subContextMenu.Padding.Z); + int largestWidth = children.Max(c => c.Rect.Width + horizontalPadding); + + // Resize all children to the size of the longest element + foreach (GUIComponent c in children) { c.RectTransform.MinSize = new Point(largestWidth, c.Rect.Height); } + + // crop the context menu + subContextMenu.RectTransform.NonScaledSize = new Point(largestWidth, children.Sum(c => c.Rect.Height) + horizontalPadding); + + // if the menu would go off the screen then move it up + if (subContextMenu.Rect.Bottom > GameMain.GraphicsHeight) + { + subContextMenu.RectTransform.ScreenSpaceOffset = new Point(pos.X, pos.Y - subContextMenu.Rect.Height); + } + + subContextMenu.OnSelected = (component, obj) => + { + if (component.Enabled && obj is PermissionPreset preset) + { + var label = TextManager.GetWithVariables(preset.Permissions == ClientPermissions.None ? "clearrankprompt" : "giverankprompt", new []{ "[user]", "[rank]" }, new []{ client.Name, preset.Name }); + + var msgBox = new GUIMessageBox(string.Empty, label, new[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); + + msgBox.Buttons[0].OnClicked = (yesBtn, userdata) => + { + client.SetPermissions(preset.Permissions, preset.PermittedCommands); + GameMain.Client.UpdateClientPermissions(client); + msgBox.Close(); + return true; + }; + msgBox.Buttons[1].OnClicked = (_, userdata) => + { + msgBox.Close(); + return true; + }; + contextMenu = null; + subContextMenu = null; + return true; + } + return false; + }; + } + + private static bool IsMouseOnContextMenu(Rectangle rect) + { + Rectangle expandedRect = rect; + expandedRect.Inflate(20, 20); + return expandedRect.Contains(PlayerInput.MousePosition); + } + + #endregion public void AddToGUIUpdateList() { @@ -899,22 +1137,17 @@ namespace Barotrauma { if (!(c.UserData is Character character) || character.IsDead || character.Removed) { continue; } AddCharacter(character); - if (c.GetChild() is GUILayoutGroup oldLayoutGroup) + if (GetPreviousOrderComponent(c.GetChild())?.UserData is OrderInfo prevInfo && + crewList.Content.Children.FirstOrDefault(c => c?.UserData == character)?.GetChild() is GUILayoutGroup newLayoutGroup) { - if (GetCurrentOrderComponent(oldLayoutGroup)?.UserData is OrderInfo currInfo) - { - DisplayCharacterOrder(character, currInfo.Order, currInfo.OrderOption); - } - if (GetPreviousOrderComponent(oldLayoutGroup)?.UserData is OrderInfo prevInfo && - crewList.Content.Children.FirstOrDefault(c => c?.UserData == character)?.GetChild() is GUILayoutGroup newLayoutGroup) - { - DisplayPreviousCharacterOrder(character, newLayoutGroup, prevInfo); - } + DisplayPreviousCharacterOrder(character, newLayoutGroup, prevInfo); } } } guiFrame.AddToGUIUpdateList(); + contextMenu?.AddToGUIUpdateList(false, 1); + subContextMenu?.AddToGUIUpdateList(false, 1); } public void SelectNextCharacter() @@ -978,6 +1211,43 @@ namespace Barotrauma SelectPreviousCharacter(); } } + + // context menu behavior + if (contextMenu != null) + { + var promote = contextMenu.GetChild()?.Content.GetChildByUserData("promote"); + + if (promote != null && promote.Enabled) + { + promote.ExternalHighlight = subContextMenu != null; + + if (GUI.IsMouseOn(promote)) + { + if (contextMenu.UserData is Client client && subContextMenu == null) + { + CreatePromoteSubMenu(new Point(promote.Rect.Right, promote.Rect.Y), client); + } + } + else if (subContextMenu != null && !IsMouseOnContextMenu(subContextMenu.Rect)) + { + subContextMenu = null; + } + } + else + { + subContextMenu = null; + } + + if (subContextMenu == null && !IsMouseOnContextMenu(contextMenu.Rect)) + { + contextMenu = null; + } + } + + if (contextMenu == null && subContextMenu != null) + { + subContextMenu = null; + } if (GUI.DisableHUD) { return; } @@ -988,7 +1258,15 @@ namespace Barotrauma if (PlayerInput.KeyDown(InputType.Command) && (GUI.KeyboardDispatcher.Subscriber == null || GUI.KeyboardDispatcher.Subscriber == crewList) && commandFrame == null && !clicklessSelectionActive && CanIssueOrders) { - CreateCommandUI(HUDLayoutSettings.PortraitArea.Contains(PlayerInput.MousePosition) ? Character.Controlled : GUI.MouseOn?.UserData as Character); + if (PlayerInput.KeyDown(Keys.LeftShift) || PlayerInput.KeyDown(Keys.RightShift)) + { + CreateCommandUI(FindEntityContext(), true); + } + else + { + CreateCommandUI(HUDLayoutSettings.PortraitArea.Contains(PlayerInput.MousePosition) ? Character.Controlled : GUI.MouseOn?.UserData as Character); + } + GUI.PlayUISound(GUISoundType.PopupMenu); clicklessSelectionActive = isOpeningClick = true; } @@ -1016,8 +1294,7 @@ namespace Barotrauma node = shortcutNodes.Find(n => GUI.IsMouseOn(n)); } // Make sure the node is for an option-less order... - if (node.UserData is Order order && - !(order.ItemComponentType != null || order.ItemIdentifiers.Length > 0 || order.Options.Length > 1)) + if (node.UserData is Order order && !order.HasOptions) { CreateAssignmentNodes(node); } @@ -1083,7 +1360,7 @@ namespace Barotrauma } } - if (closestNode == selectedNode) + if (closestNode != null && closestNode == selectedNode) { timeSelected += deltaTime; if (timeSelected >= selectionTime) @@ -1154,8 +1431,6 @@ namespace Barotrauma #endregion - if (GUI.DisableUpperHUD) { return; } - if (ChatBox != null) { ChatBox.Update(deltaTime); @@ -1197,48 +1472,52 @@ namespace Barotrauma } } - crewArea.Visible = characters.Count > 0 && CharacterHealth.OpenHealthWindow == null; - - foreach (GUIComponent child in crewList.Content.Children) + if (!GUI.DisableUpperHUD) { - if (child.UserData is Character character) + crewArea.Visible = characters.Count > 0 && CharacterHealth.OpenHealthWindow == null; + + foreach (GUIComponent child in crewList.Content.Children) { - child.Visible = Character.Controlled == null || Character.Controlled.TeamID == character.TeamID; - if (child.Visible) + if (child.UserData is Character character) { - if (character == Character.Controlled && child.State != GUIComponent.ComponentState.Selected) + child.Visible = Character.Controlled == null || Character.Controlled.TeamID == character.TeamID; + if (child.Visible) { - crewList.Select(character, force: true); - } - if (child.FindChild(c => c is GUILayoutGroup) is GUILayoutGroup layoutGroup) - { - if (GetCurrentOrderComponent(layoutGroup) is GUIComponent orderButton && - orderButton.GetChildByUserData("colorsource") is GUIComponent orderIcon && - orderButton.GetChildByUserData("cancel") is GUIComponent cancelIcon) + if (character == Character.Controlled && child.State != GUIComponent.ComponentState.Selected) { - cancelIcon.Visible = GUI.IsMouseOn(orderIcon); + crewList.Select(character, force: true); } - if (layoutGroup.GetChildByUserData("soundicons")? - .FindChild(c => c.UserData is Pair pair && pair.First == "soundicon") is GUIImage soundIcon) + if (child.FindChild(c => c is GUILayoutGroup) is GUILayoutGroup layoutGroup) { - VoipClient.UpdateVoiceIndicator(soundIcon, 0.0f, deltaTime); + if (GetCurrentOrderComponent(layoutGroup) is GUIComponent orderButton && + orderButton.GetChildByUserData("colorsource") is GUIComponent orderIcon && + orderButton.GetChildByUserData("cancel") is GUIComponent cancelIcon) + { + cancelIcon.Visible = GUI.IsMouseOn(orderIcon); + } + if (layoutGroup.GetChildByUserData("soundicons")? + .FindChild(c => c.UserData is Pair pair && pair.First == "soundicon") is GUIImage soundIcon) + { + VoipClient.UpdateVoiceIndicator(soundIcon, 0.0f, deltaTime); + } } } } } - } - crewArea.RectTransform.AbsoluteOffset = Vector2.SmoothStep( - new Vector2(-crewArea.Rect.Width - HUDLayoutSettings.Padding, 0.0f), - Vector2.Zero, - crewListOpenState).ToPoint(); - crewListOpenState = ToggleCrewListOpen ? - Math.Min(crewListOpenState + deltaTime * 2.0f, 1.0f) : - Math.Max(crewListOpenState - deltaTime * 2.0f, 0.0f); + crewArea.RectTransform.AbsoluteOffset = Vector2.SmoothStep( + new Vector2(-crewArea.Rect.Width - HUDLayoutSettings.Padding, 0.0f), + Vector2.Zero, + crewListOpenState).ToPoint(); + crewListOpenState = ToggleCrewListOpen ? + Math.Min(crewListOpenState + deltaTime * 2.0f, 1.0f) : + Math.Max(crewListOpenState - deltaTime * 2.0f, 0.0f); - if (GUI.KeyboardDispatcher.Subscriber == null && PlayerInput.KeyHit(InputType.CrewOrders)) - { - ToggleCrewListOpen = !ToggleCrewListOpen; + if (GUI.KeyboardDispatcher.Subscriber == null && PlayerInput.KeyHit(InputType.CrewOrders)) + { + GUI.PlayUISound(GUISoundType.PopupMenu); + ToggleCrewListOpen = !ToggleCrewListOpen; + } } UpdateReports(); @@ -1291,7 +1570,12 @@ namespace Barotrauma private float returnNodeDistanceModifier = 0.65f; private Order dismissedOrderPrefab; private Character characterContext; + private Item itemContext; + private Hull hullContext; + private bool isContextual; + private readonly List contextualOrders = new List(); private Point shorcutCenterNodeOffset; + private const int maxShorcutNodeCount = 4; private bool WasCommandInterfaceDisabledThisUpdate { get; set; } private bool CanIssueOrders { @@ -1314,11 +1598,65 @@ namespace Barotrauma #endif } - private void CreateCommandUI(Character characterContext = null) + private Entity FindEntityContext() + { + if (Character.Controlled?.FocusedCharacter != null) + { + if (Character.Controlled?.FocusedItem != null) + { + Vector2 mousePos = GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition); + if (Vector2.Distance(mousePos, Character.Controlled.FocusedCharacter.WorldPosition) < Vector2.Distance(mousePos, Character.Controlled.FocusedItem.WorldPosition)) + { + return Character.Controlled.FocusedCharacter; + } + else + { + return Character.Controlled.FocusedItem; + } + } + else + { + return Character.Controlled.FocusedCharacter; + } + + } + else if (TryGetBreachedHullAtHoveredWall(out Hull breachedHull)) + { + return breachedHull; + } + else + { + return Character.Controlled?.FocusedItem; + } + } + + private void CreateCommandUI(Entity entityContext = null, bool forceContextual = false) { if (commandFrame != null) { DisableCommandUI(); } + CharacterHealth.OpenHealthWindow = null; + + // Character context works differently to others as we still use the "basic" command interface, + // but the order will be automatically assigned to this character + isContextual = forceContextual; + if (entityContext is Character character) + { + characterContext = character; + isContextual = false; + } + else if (entityContext is Item item) + { + itemContext = item; + isContextual = true; + } + else if (entityContext is Hull hull) + { + hullContext = hull; + isContextual = true; + } + ScaleCommandUI(); + commandFrame = new GUIFrame( new RectTransform(Vector2.One, GUICanvas.Instance, anchor: Anchor.Center), style: null, @@ -1327,8 +1665,6 @@ namespace Barotrauma new RectTransform(Vector2.One, commandFrame.RectTransform, anchor: Anchor.Center), "CommandBackground"); background.Color = background.Color * 0.8f; - - this.characterContext = characterContext; GUIButton startNode = null; if (characterContext == null) { @@ -1358,7 +1694,11 @@ namespace Barotrauma new RectTransform(Vector2.One, startNode.RectTransform, anchor: Anchor.Center), (spriteBatch, _) => { - characterContext.Info.DrawIcon(spriteBatch, startNode.Center, startNode.Rect.Size.ToVector2() * 0.6f); + if (!(entityContext is Character character)) { return; } + var node = startNode; + character.Info.DrawJobIcon(spriteBatch, + new Rectangle((int)(node.Rect.X + node.Rect.Width * 0.5f), (int)(node.Rect.Y + node.Rect.Height * 0.1f), (int)(node.Rect.Width * 0.6f), (int)(node.Rect.Height * 0.8f))); + character.Info.DrawIcon(spriteBatch, new Vector2(node.Rect.X + node.Rect.Width * 0.35f, node.Center.Y), node.Rect.Size.ToVector2() * 0.7f); }) { ToolTip = characterContext.Info.DisplayName + " (" + characterContext.Info.Job.Name + ")" @@ -1369,8 +1709,16 @@ namespace Barotrauma availableCategories ??= GetAvailableCategories(); dismissedOrderPrefab ??= Order.GetPrefab("dismissed"); - CreateShortcutNodes(); - CreateOrderCategoryNodes(); + if (isContextual) + { + CreateContextualOrderNodes(); + } + else + { + CreateShortcutNodes(); + CreateOrderCategoryNodes(); + } + CreateNodeConnectors(); if (Character.Controlled != null) { @@ -1414,7 +1762,6 @@ namespace Barotrauma availableCategories = new List(); foreach (OrderCategory category in Enum.GetValues(typeof(OrderCategory))) { - if (category == OrderCategory.Undefined) { continue; } if (Order.PrefabList.Any(o => o.Category == category && !o.TargetAllCharacters)) { availableCategories.Add(category); @@ -1486,6 +1833,9 @@ namespace Barotrauma extraOptionCharacters.Clear(); isOpeningClick = isSelectionHighlighted = false; characterContext = null; + itemContext = null; + isContextual = false; + contextualOrders.Clear(); returnNodeHotkey = expandNodeHotkey = Keys.None; if (Character.Controlled != null) { @@ -1601,8 +1951,15 @@ namespace Barotrauma { if (userData == null) { - CreateShortcutNodes(); - CreateOrderCategoryNodes(); + if (isContextual) + { + CreateContextualOrderNodes(); + } + else + { + CreateShortcutNodes(); + CreateOrderCategoryNodes(); + } } else if (userData is OrderCategory category) { @@ -1671,8 +2028,7 @@ namespace Barotrauma shortcutNodes.Clear(); - var reactor = sub.GetItems(false).Find(i => i.HasTag("reactor"))?.GetComponent(); - if (reactor != null) + if (shortcutNodes.Count < maxShorcutNodeCount && sub.GetItems(false).Find(i => i.HasTag("reactor"))?.GetComponent() is Reactor reactor) { var reactorOutput = -reactor.CurrPowerConsumption; // If player is not an engineer AND the reactor is not powered up AND nobody is using the reactor @@ -1689,7 +2045,7 @@ namespace Barotrauma // TODO: Reconsider the conditions as bot captain can have the nav term selected without operating it // If player is not a captain AND nobody is using the nav terminal AND the nav terminal is powered up // --> Create shortcut node for Steer order - if ((Character.Controlled == null || Character.Controlled.Info.Job.Prefab != JobPrefab.Get("captain")) && + if (shortcutNodes.Count < maxShorcutNodeCount && (Character.Controlled == null || Character.Controlled.Info.Job.Prefab != JobPrefab.Get("captain")) && sub.GetItems(false).Find(i => i.HasTag("navterminal")) is Item nav && characters.None(c => c.SelectedConstruction == nav) && nav.GetComponent() is Steering steering && steering.Voltage > steering.MinVoltage) { @@ -1699,7 +2055,7 @@ namespace Barotrauma // If player is not a security officer AND invaders are reported // --> Create shorcut node for Fight Intruders order - if ((Character.Controlled == null || Character.Controlled.Info.Job.Prefab != JobPrefab.Get("securityofficer")) && + if (shortcutNodes.Count < maxShorcutNodeCount && (Character.Controlled == null || Character.Controlled.Info.Job.Prefab != JobPrefab.Get("securityofficer")) && (Order.GetPrefab("reportintruders") is Order reportIntruders && ActiveOrders.Any(o => o.First.Prefab == reportIntruders))) { shortcutNodes.Add( @@ -1708,7 +2064,7 @@ namespace Barotrauma // If player is not a mechanic AND a breach has been reported // --> Create shorcut node for Fix Leaks order - if ((Character.Controlled == null || Character.Controlled.Info.Job.Prefab != JobPrefab.Get("mechanic")) && + if (shortcutNodes.Count < maxShorcutNodeCount && (Character.Controlled == null || Character.Controlled.Info.Job.Prefab != JobPrefab.Get("mechanic")) && (Order.GetPrefab("reportbreach") is Order reportBreach && ActiveOrders.Any(o => o.First.Prefab == reportBreach))) { shortcutNodes.Add( @@ -1717,7 +2073,7 @@ namespace Barotrauma // If player is not an engineer AND broken devices have been reported // --> Create shortcut node for Repair Damaged Systems order - if ((Character.Controlled == null || Character.Controlled.Info.Job.Prefab != JobPrefab.Get("engineer")) && + if (shortcutNodes.Count < maxShorcutNodeCount && (Character.Controlled == null || Character.Controlled.Info.Job.Prefab != JobPrefab.Get("engineer")) && (Order.GetPrefab("reportbrokendevices") is Order reportBrokenDevices && ActiveOrders.Any(o => o.First.Prefab == reportBrokenDevices))) { shortcutNodes.Add( @@ -1726,12 +2082,27 @@ namespace Barotrauma // If fire is reported // --> Create shortcut node for Extinguish Fires order - if (ActiveOrders.Any(o=> o.First.Prefab == Order.GetPrefab("reportfire"))) + if (shortcutNodes.Count < maxShorcutNodeCount && ActiveOrders.Any(o=> o.First.Prefab == Order.GetPrefab("reportfire"))) { shortcutNodes.Add( CreateOrderNode(shortcutNodeSize, null, Point.Zero, Order.GetPrefab("extinguishfires"), -1)); } + if (shortcutNodes.Count < maxShorcutNodeCount && characterContext?.Info?.Job?.Prefab?.AppropriateOrders != null) + { + foreach (string orderIdentifier in characterContext.Info.Job.Prefab.AppropriateOrders) + { + if (Order.GetPrefab(orderIdentifier) is Order orderPrefab && + shortcutNodes.None(n => (n.UserData is Order order && order.Identifier == orderIdentifier) || + (n.UserData is Tuple orderWithOption && orderWithOption.Item1.Identifier == orderIdentifier)) && + !orderPrefab.TargetAllCharacters && orderPrefab.Category != null) + { + shortcutNodes.Add(CreateOrderNode(shortcutNodeSize, null, Point.Zero, orderPrefab, -1)); + if (shortcutNodes.Count >= maxShorcutNodeCount) { break; } + } + } + } + if (shortcutNodes.Count < 1) { return; } shortcutCenterNode = new GUIButton( @@ -1748,16 +2119,19 @@ namespace Barotrauma var nodeCountForCalculations = shortcutNodes.Count * 2 + 2; var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, 0.75f * nodeDistance, nodeCountForCalculations); + var firstOffsetIndex = nodeCountForCalculations / 2 - 1; for (int i = 0; i < shortcutNodes.Count; i++) { shortcutNodes[i].RectTransform.Parent = commandFrame.RectTransform; - shortcutNodes[i].RectTransform.MoveOverTime(shorcutCenterNodeOffset + offsets[i + 1].ToPoint(), CommandNodeAnimDuration); + shortcutNodes[i].RectTransform.MoveOverTime(shorcutCenterNodeOffset + offsets[firstOffsetIndex - i].ToPoint(), CommandNodeAnimDuration); } } private void CreateOrderNodes(OrderCategory orderCategory) { + // TODO: Save the available orders for each category so we don't have to check them again during the same game? var orders = Order.PrefabList.FindAll(o => o.Category == orderCategory && !o.TargetAllCharacters); + orders.RemoveAll(o => (o.ItemComponentType != null || o.ItemIdentifiers.Length > 0) && o.MustSetTarget && o.GetMatchingItems(true).None()); var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, GetCircumferencePointCount(orders.Count), GetFirstNodeAngle(orders.Count)); for (int i = 0; i < orders.Count; i++) @@ -1768,6 +2142,103 @@ namespace Barotrauma } } + /// + /// Create order nodes based on the item context + /// + private void CreateContextualOrderNodes() + { + if (contextualOrders.None()) + { + // Check if targeting an item or a hull + if (itemContext != null) + { + foreach (Order p in Order.PrefabList) + { + if ((p.ItemIdentifiers.Length > 0 && (p.ItemIdentifiers.Contains(itemContext.Prefab.Identifier) || itemContext.HasTag(p.ItemIdentifiers))) || + (p.ItemComponentType != null && itemContext.Components.Any(c => c?.GetType() == p.ItemComponentType))) + { + contextualOrders.Add(p.HasOptions ? p : + new Order(p, itemContext, itemContext.Components.FirstOrDefault(c => c?.GetType() == p.ItemComponentType), Character.Controlled)); + } + } + + // If targeting a periscope connected to a turret, show the 'operateweapons' order + var orderIdentifier = "operateweapons"; + var operateWeaponsPrefab = Order.GetPrefab(orderIdentifier); + if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier)) && itemContext.Components.Any(c => c is Controller) && + (itemContext.GetConnectedComponents().Any(c => operateWeaponsPrefab.ItemIdentifiers.Contains(c.Item.Prefab.Identifier)) || + itemContext.GetConnectedComponents(recursive: true).Any(c => operateWeaponsPrefab.ItemIdentifiers.Contains(c.Item.Prefab.Identifier)))) + { + contextualOrders.Add(operateWeaponsPrefab); + } + + // If targeting a repairable item, show the 'repairsystems' order + orderIdentifier = "repairsystems"; + if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier)) && itemContext.Repairables.Any()) + { + contextualOrders.Add(new Order(Order.GetPrefab(orderIdentifier), itemContext, null, Character.Controlled)); + if (itemContext.Repairables.Any(r => r != null && r.requiredSkills.Any(s => s != null && s.Identifier.Equals("electrical")))) + { + contextualOrders.Add(new Order(Order.GetPrefab("repairelectrical"), itemContext, null, Character.Controlled)); + } + else if (itemContext.Repairables.Any(r => r != null && r.requiredSkills.Any(s => s != null && s.Identifier.Equals("mechanical")))) + { + contextualOrders.Add(new Order(Order.GetPrefab("repairmechanical"), itemContext, null, Character.Controlled)); + } + } + + // Always show the 'wait' order if there are other crew members alive + orderIdentifier = "wait"; + if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier)) && characters.Any(c => c != Character.Controlled)) + { + contextualOrders.Add(new Order(Order.GetPrefab(orderIdentifier), itemContext, null, Character.Controlled)); + } + } + else if(hullContext != null) + { + contextualOrders.Add(new Order(Order.GetPrefab("fixleaks"), hullContext, null, Character.Controlled)); + } + + // Show the 'follow' and 'dismissed' orders if there are other crew members alive + if (characters.Any(c => c != Character.Controlled)) + { + var orderIdentifier = "follow"; + if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier))) + { + contextualOrders.Add(Order.GetPrefab(orderIdentifier)); + } + // Show 'dismissed' order only when there are crew members with active orders + orderIdentifier = "dismissed"; + if (contextualOrders.None(o => o.Identifier.Equals(orderIdentifier)) && + characters.Any(c => c.CurrentOrder != null && !c.CurrentOrder.Identifier.Equals(orderIdentifier))) + { + contextualOrders.Add(Order.GetPrefab(orderIdentifier)); + } + } + } + + var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, contextualOrders.Count, MathHelper.ToRadians(90f + 180f / contextualOrders.Count)); + for (int i = 0; i < contextualOrders.Count; i++) + { + optionNodes.Add(new Tuple( + CreateOrderNode(nodeSize, commandFrame.RectTransform, offsets[i].ToPoint(), contextualOrders[i], (i + 1) % 10), + CanSomeoneHearCharacter() ? Keys.D0 + (i + 1) % 10 : Keys.None)); + } + } + + public static bool DoesItemHaveContextualOrders(Item item) + { + if (Order.PrefabList.Any(o => o.ItemIdentifiers.Length > 0 && o.ItemIdentifiers.Contains(item.Prefab.Identifier))) { return true; } + if (Order.PrefabList.Any(o => item.HasTag(o.ItemIdentifiers))) { return true; } + if (Order.PrefabList.Any(o => o.ItemComponentType != null && item.Components.Any(c => c?.GetType() == o.ItemComponentType))) { return true; } + + if (item.Repairables.Any()) { return true; } + var operateWeaponsPrefab = Order.GetPrefab("operateweapons"); + return item.Components.Any(c => c is Controller) && + (item.GetConnectedComponents().Any(c => operateWeaponsPrefab.ItemIdentifiers.Contains(c.Item.Prefab.Identifier)) || + item.GetConnectedComponents(recursive: true).Any(c => operateWeaponsPrefab.ItemIdentifiers.Contains(c.Item.Prefab.Identifier))); + } + private GUIButton CreateOrderNode(Point size, RectTransform parent, Point offset, Order order, int hotkey) { var node = new GUIButton( @@ -1779,29 +2250,27 @@ namespace Barotrauma node.RectTransform.MoveOverTime(offset, CommandNodeAnimDuration); var canSomeoneHearCharacter = CanSomeoneHearCharacter(); - var hasOptions = order.ItemComponentType != null || order.ItemIdentifiers.Length > 0 || order.Options.Length > 1; node.OnClicked = (button, userData) => { if (!canSomeoneHearCharacter || !CanIssueOrders) { return false; } var o = userData as Order; - // TODO: Consider defining orders' or order categories' quick-assignment possibility in the XML - if (o.Category == OrderCategory.Movement && characterContext == null) + if (o.MustManuallyAssign && characterContext == null) { CreateAssignmentNodes(node); } - else if (hasOptions) + else if (o.HasOptions) { NavigateForward(button, userData); } else { - SetCharacterOrder(characterContext ?? GetBestCharacterForOrder(o), o, null, Character.Controlled); + SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o), o, null, Character.Controlled); DisableCommandUI(); } return true; }; var icon = CreateNodeIcon(node.RectTransform, order.SymbolSprite, order.Color, - tooltip: hasOptions || characterContext != null ? order.Name : order.Name + + tooltip: order.HasOptions || characterContext != null ? order.Name : order.Name + "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.leftmouse") : TextManager.Get("input.rightmouse")) + ": " + TextManager.Get("commandui.quickassigntooltip") + "\n" + (!PlayerInput.MouseButtonsSwapped() ? TextManager.Get("input.rightmouse") : TextManager.Get("input.leftmouse")) + ": " + TextManager.Get("commandui.manualassigntooltip")); @@ -1824,20 +2293,10 @@ namespace Barotrauma Submarine submarine = Character.Controlled != null && Character.Controlled.TeamID == Character.TeamType.Team2 && Submarine.MainSubs.Length > 1 ? Submarine.MainSubs[1] : Submarine.MainSub; - - List matchingItems = new List(); - if (order.ItemComponentType != null || order.ItemIdentifiers.Length > 0) - { - matchingItems = order.ItemIdentifiers.Length > 0 ? - Item.ItemList.FindAll(it => order.ItemIdentifiers.Contains(it.Prefab.Identifier) || it.HasTag(order.ItemIdentifiers)) : - Item.ItemList.FindAll(it => it.Components.Any(ic => ic.GetType() == order.ItemComponentType)); - - matchingItems.RemoveAll(it => it.Submarine != submarine && !submarine.DockedTo.Contains(it.Submarine)); - matchingItems.RemoveAll(it => it.Submarine != null && it.Submarine.IsOutpost); - } + var matchingItems = (itemContext == null && order.MustSetTarget) ? order.GetMatchingItems(submarine, true) : new List(); //more than one target item -> create a minimap-like selection with a pic of the sub - if (matchingItems.Count > 1) + if (itemContext == null && matchingItems.Count > 1) { // TODO: Further adjustments to frameSize calculations // I just divided the existing sizes by 2 to get it working quickly without it overlapping too much @@ -1930,7 +2389,7 @@ namespace Barotrauma { if (!CanIssueOrders) { return false; } var o = userData as Tuple; - SetCharacterOrder(characterContext ?? GetBestCharacterForOrder(o.Item1), o.Item1, o.Item2, Character.Controlled); + SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o.Item1), o.Item1, o.Item2, Character.Controlled); DisableCommandUI(); return true; } @@ -1950,8 +2409,10 @@ namespace Barotrauma //only one target (or an order with no particular targets), just show options else { - var item = matchingItems.Count > 0 ? matchingItems[0] : null; - var o = item == null ? order : new Order(order, item, item.Components.FirstOrDefault(ic => ic.GetType() == order.ItemComponentType)); + var item = itemContext != null ? + (order.UseController ? itemContext.GetConnectedComponents().FirstOrDefault()?.Item ?? itemContext.GetConnectedComponents(recursive: true).FirstOrDefault()?.Item : itemContext) : + (matchingItems.Count > 0 ? matchingItems[0] : null); + var o = item == null || !order.IsPrefab ? order : new Order(order, item, item.Components.FirstOrDefault(ic => ic.GetType() == order.ItemComponentType)); var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, GetCircumferencePointCount(order.Options.Length), GetFirstNodeAngle(order.Options.Length)); @@ -1979,7 +2440,7 @@ namespace Barotrauma { if (!CanIssueOrders) { return false; } var o = userData as Tuple; - SetCharacterOrder(characterContext ?? GetBestCharacterForOrder(o.Item1), o.Item1, o.Item2, Character.Controlled); + SetCharacterOrder(characterContext ?? GetCharacterForQuickAssignment(o.Item1), o.Item1, o.Item2, Character.Controlled); DisableCommandUI(); return true; } @@ -2010,7 +2471,7 @@ namespace Barotrauma var order = (node.UserData is Order) ? new Tuple(node.UserData as Order, null) : node.UserData as Tuple; - var characters = GetCharactersSortedForOrder(order.Item1); + var characters = GetCharactersForManualAssignment(order.Item1); if (characters.Count < 1) { return; } if (!(optionNodes.Find(n => n.Item1 == node) is Tuple optionNode) || !optionNodes.Remove(optionNode)) @@ -2153,7 +2614,10 @@ namespace Barotrauma new RectTransform(Vector2.One, node.RectTransform), (spriteBatch, _) => { - character.Info.DrawIcon(spriteBatch, node.Center, node.Rect.Size.ToVector2() * 0.75f); + character.Info.DrawJobIcon(spriteBatch, + new Rectangle((int)(node.Rect.X + node.Rect.Width * 0.5f), (int)(node.Rect.Y + node.Rect.Height * 0.1f), (int)(node.Rect.Width * 0.6f), (int)(node.Rect.Height * 0.8f))); + character.Info.DrawIcon(spriteBatch, new Vector2(node.Rect.X + node.Rect.Width * 0.35f, node.Center.Y), node.Rect.Size.ToVector2() * 0.7f); + }) { ToolTip = character.Info.DisplayName + " (" + character.Info.Job.Name + ")" @@ -2282,135 +2746,63 @@ namespace Barotrauma return (degrees < 0) ? (degrees + 360) : degrees; } + private bool TryGetBreachedHullAtHoveredWall(out Hull breachedHull) + { + breachedHull = null; + List leaks = Gap.GapList.FindAll(g => + g != null && g.ConnectedWall != null && g.ConnectedDoor == null && g.Open > 0 && g.linkedTo.Any(l => l != null) && + g.Submarine != null && (Character.Controlled != null && g.Submarine.TeamID == Character.Controlled.TeamID && g.Submarine.Info.IsPlayer)); + if (leaks.None()) { return false; } + Vector2 mouseWorldPosition = GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition); + foreach (Gap leak in leaks) + { + if (Submarine.RectContains(leak.ConnectedWall.WorldRect, mouseWorldPosition)) + { + breachedHull = leak.FlowTargetHull; + return true; + } + } + return false; + } + #region Crew Member Assignment Logic - private Character GetBestCharacterForOrder(Order order) + private Character GetCharacterForQuickAssignment(Order order) { #if !DEBUG if (Character.Controlled == null) { return null; } #endif - if (order.Category == OrderCategory.Operate && AIObjectiveOperateItem.IsOperatedByAnother(null, order.TargetItemComponent, out Character operatingCharacter)) + if (order.Category == OrderCategory.Operate && HumanAIController.IsItemOperatedByAnother(null, order.TargetItemComponent, out Character operatingCharacter)) { return operatingCharacter; } - return characters.FindAll(c => Character.Controlled == null || (c != Character.Controlled && c.TeamID == Character.Controlled.TeamID)) - .OrderByDescending(c => c.CurrentOrder == null || c.CurrentOrder.Identifier == dismissedOrderPrefab.Identifier) - .ThenByDescending(c => order.HasAppropriateJob(c)) - .ThenBy(c => c.CurrentOrder?.Weight) - .FirstOrDefault(); + return GetCharactersSortedForOrder(order, false).FirstOrDefault(); } - private List GetCharactersSortedForOrder(Order order) + private List GetCharactersForManualAssignment(Order order) { #if !DEBUG if (Character.Controlled == null) { return new List(); } #endif - if (order.Identifier == "follow") - { - return characters.FindAll(c => Character.Controlled == null || (c != Character.Controlled && c.TeamID == Character.Controlled.TeamID)) - .OrderByDescending(c => c.CurrentOrder == null || c.CurrentOrder.Identifier == dismissedOrderPrefab.Identifier) - .ToList(); - } - else - { - return characters.FindAll(c => Character.Controlled == null || c.TeamID == Character.Controlled.TeamID) - .OrderByDescending(c => c.CurrentOrder == null || c.CurrentOrder.Identifier == dismissedOrderPrefab.Identifier) + return GetCharactersSortedForOrder(order, order.Identifier != "follow").ToList(); + } + + private IEnumerable GetCharactersSortedForOrder(Order order, bool includeSelf) + { + return characters.FindAll(c => Character.Controlled == null || ((includeSelf || c != Character.Controlled) && c.TeamID == Character.Controlled.TeamID)) + .OrderByDescending(c => c.CurrentOrder != null && order.Category == OrderCategory.Operate && c.CurrentOrder.Identifier == order.Identifier && c.CurrentOrder.TargetEntity == order.TargetEntity) + .ThenByDescending(c => c.CurrentOrder == null || c.CurrentOrder.Identifier == dismissedOrderPrefab.Identifier) + .ThenBy(c => c.CurrentOrder != null && c.CurrentOrder.Identifier == order.Identifier && c.CurrentOrder.TargetEntity == order.TargetEntity) .ThenByDescending(c => order.HasAppropriateJob(c)) .ThenBy(c => c.CurrentOrder?.Weight) - .ToList(); - } + .ThenByDescending(c => c.GetSkillLevel(order.AppropriateSkill)); } -#endregion + #endregion -#endregion + #endregion - /// - /// Creates a listbox that includes all the characters in the crew, can be used externally (round info menus etc) - /// - public void CreateCrewListFrame(IEnumerable crew, GUIFrame crewFrame) - { - List teamIDs = crew.Select(c => c.TeamID).Distinct().ToList(); - - if (!teamIDs.Any()) teamIDs.Add(Character.TeamType.None); - - int listBoxHeight = 300 / teamIDs.Count; - - var content = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), crewFrame.RectTransform)) - { - Stretch = true - }; - - for (int i = 0; i < teamIDs.Count; i++) - { - if (teamIDs.Count > 1) - { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), content.RectTransform), CombatMission.GetTeamName(teamIDs[i])); - } - - GUIListBox crewList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.1f) }) - { - UserData = crewFrame - }; - crewList.OnSelected = (component, obj) => - { - SelectCrewCharacter(component.UserData as Character, crewList); - return true; - }; - - foreach (Character character in crew.Where(c => c.TeamID == teamIDs[i])) - { - GUIFrame frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.15f), crewList.Content.RectTransform), style: "ListBoxElement") - { - UserData = character, - Color = (GameMain.NetworkMember != null && GameMain.Client.Character == character) ? Color.Gold * 0.2f : Color.Transparent - }; - - var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: true) - { - RelativeSpacing = 0.05f, - Stretch = true - }; - - new GUICustomComponent(new RectTransform(new Vector2(0.2f, 1.0f), paddedFrame.RectTransform, Anchor.CenterLeft), - onDraw: (sb, component) => character.Info.DrawIcon(sb, component.Rect.Center.ToVector2(), targetAreaSize: component.Rect.Size.ToVector2())) - { - CanBeFocused = false, - HoverColor = Color.White, - SelectedColor = Color.White - }; - - GUITextBlock textBlock = new GUITextBlock(new RectTransform(Vector2.One, paddedFrame.RectTransform), - ToolBox.LimitString(character.Info.Name + " (" + character.Info.Job.Name + ")", GUI.Font, paddedFrame.Rect.Width - paddedFrame.Rect.Height), - textColor: character.Info.Job.Prefab.UIColor); - } - } - } - - /// - /// Select a character from CrewListFrame - /// - protected bool SelectCrewCharacter(Character character, GUIComponent crewList) - { - if (character == null) { return false; } - - GUIComponent crewFrame = (GUIComponent)crewList.UserData; - GUIComponent existingPreview = crewFrame.FindChild("SelectedCharacter"); - if (existingPreview != null) { crewFrame.RemoveChild(existingPreview); } - - var previewPlayer = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.7f), crewFrame.RectTransform, Anchor.TopRight), style: null) - { - UserData = "SelectedCharacter" - }; - - character.Info.CreateInfoFrame(previewPlayer); - - if (GameMain.NetworkMember != null) { GameMain.Client.SelectCrewCharacter(character, previewPlayer); } - - return true; - } - -#region Reports + #region Reports /// /// Enables/disables report buttons when needed @@ -2421,7 +2813,11 @@ namespace Barotrauma if (Character.Controlled?.CurrentHull?.Submarine != null && Character.Controlled.SpeechImpediment < 100.0f) { WifiComponent radio = GetHeadset(Character.Controlled, true); - canIssueOrders = radio != null && radio.CanTransmit(); + canIssueOrders = + radio != null && + radio.CanTransmit() && + Character.Controlled?.CurrentHull?.Submarine?.TeamID == Character.Controlled.TeamID && + !Character.Controlled.CurrentHull.Submarine.Info.IsWreck; } if (canIssueOrders) @@ -2430,6 +2826,7 @@ namespace Barotrauma ReportButtonFrame.Visible = !Character.Controlled.ShouldLockHud() && (Character.Controlled?.SelectedCharacter?.Inventory == null || !Character.Controlled.SelectedCharacter.CanInventoryBeAccessed); + if (!ReportButtonFrame.Visible) { return; } var reportButtonParent = ChatBox ?? GameMain.Client?.ChatBox; if (reportButtonParent == null) { return; } @@ -2439,7 +2836,7 @@ namespace Barotrauma bool hasFires = Character.Controlled.CurrentHull.FireSources.Count > 0; ToggleReportButton("reportfire", hasFires); - bool hasLeaks = Character.Controlled.CurrentHull.Submarine != null && Character.Controlled.CurrentHull.ConnectedGaps.Any(g => !g.IsRoomToRoom && g.Open > 0.0f); + bool hasLeaks = Character.Controlled.CurrentHull.ConnectedGaps.Any(g => !g.IsRoomToRoom && g.Open > 0.0f); ToggleReportButton("reportbreach", hasLeaks); bool hasIntruders = Character.CharacterList.Any(c => c.CurrentHull == Character.Controlled.CurrentHull && AIObjectiveFightIntruders.IsValidTarget(c, Character.Controlled)); @@ -2470,7 +2867,7 @@ namespace Barotrauma } } -#endregion + #endregion public void InitSinglePlayerRound() { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs index e73d93e9c..06ba7d188 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -134,7 +134,7 @@ namespace Barotrauma foreach (PurchasedItem pi in CargoManager.PurchasedItems) { msg.Write(pi.ItemPrefab.Identifier); - msg.Write((UInt16)pi.Quantity); + msg.WriteRangedInteger(pi.Quantity, 0, 100); } } @@ -162,7 +162,7 @@ namespace Barotrauma for (int i = 0; i < purchasedItemCount; i++) { string itemPrefabIdentifier = msg.ReadString(); - UInt16 itemQuantity = msg.ReadUInt16(); + int itemQuantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity); purchasedItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity)); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index 06dca83df..25f8abb4a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -276,7 +276,11 @@ namespace Barotrauma c.SaveInventory(c.Inventory, inventoryElement); c.Info.InventoryData = inventoryElement; c.Inventory?.DeleteAllItems(); + c.ResetCurrentOrder(); } + + GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); + SaveUtil.SaveGame(GameMain.GameSession.SavePath); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SubTestMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SubTestMode.cs new file mode 100644 index 000000000..cfddadd7e --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SubTestMode.cs @@ -0,0 +1,80 @@ +using Barotrauma.Tutorials; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + class SubTestMode : GameMode + { + public SubTestMode(GameModePreset preset, object param) + : base(preset, param) + { + foreach (JobPrefab jobPrefab in JobPrefab.Prefabs) + { + for (int i = 0; i < jobPrefab.InitialCount; i++) + { + var variant = Rand.Range(0, jobPrefab.Variants); + CrewManager.AddCharacterInfo(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: jobPrefab, variant: variant)); + } + } + } + + public override void Start() + { + base.Start(); + + isRunning = true; + CrewManager.InitSinglePlayerRound(); + + Submarine.MainSub.SetPosition(Vector2.Zero); + } + + public override void Draw(SpriteBatch spriteBatch) + { + if (!isRunning|| GUI.DisableHUD || GUI.DisableUpperHUD) return; + + if (Submarine.MainSub == null) return; + } + + public override void AddToGUIUpdateList() + { + if (!isRunning) return; + + base.AddToGUIUpdateList(); + CrewManager.AddToGUIUpdateList(); + } + + public override void Update(float deltaTime) + { + if (!isRunning) { return; } + + base.Update(deltaTime); + } + + public override void End(string endMessage = "") + { + isRunning = false; + + GameMain.GameSession.EndRound(""); + + CrewManager.EndRound(); + + Submarine.Unload(); + + GameMain.SubEditorScreen.Select(); + } + + private bool EndRound(Submarine leavingSub) + { + isRunning = false; + + End(""); + + return true; + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs index 89c5fbf6c..8825ec280 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/CaptainTutorial.cs @@ -246,7 +246,7 @@ namespace Barotrauma.Tutorials { //captain_navConsoleCustomInterface.HighlightElement(0, uiHighlightColor, duration: 1.0f, pulsateAmount: 0.0f); yield return new WaitForSeconds(1.0f, false); - } while (!Submarine.MainSub.AtEndPosition || Submarine.MainSub.DockedTo.Any()); + } while (!Submarine.MainSub.AtEndPosition || !Submarine.MainSub.DockedTo.Any()); RemoveCompletedObjective(segments[6]); yield return new WaitForSeconds(3f, false); GameMain.GameSession?.CrewManager.AddSinglePlayerChatMessage(radioSpeakerName, TextManager.GetWithVariable("Captain.Radio.Complete", "[OUTPOSTNAME]", GameMain.GameSession.EndLocation.Name), ChatMessageType.Radio, null); @@ -284,7 +284,9 @@ namespace Barotrauma.Tutorials private bool IsSelectedItem(Item item) { - return captain?.SelectedConstruction == item; + return + captain?.SelectedConstruction == item || + (captain?.SelectedConstruction?.linkedTo?.Contains(item) ?? false); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs index 28999e871..d4717ab96 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/DoctorTutorial.cs @@ -156,7 +156,7 @@ namespace Barotrauma.Tutorials //yield return new WaitForSeconds(2.5f); doctor.SetStun(1.5f); - var explosion = new Explosion(range: 100, force: 10, damage: 0, structureDamage: 0); + var explosion = new Explosion(range: 100, force: 10, damage: 0, structureDamage: 0, itemDamage: 0); explosion.DisableParticles(); GameMain.GameScreen.Cam.Shake = shakeAmount; explosion.Explode(Character.Controlled.WorldPosition - Vector2.UnitX * 25, null); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs index 8434baceb..d659ef6ea 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/ScenarioTutorial.cs @@ -21,8 +21,8 @@ namespace Barotrauma.Tutorials private string levelSeed; private string levelParams; - private Submarine startOutpost = null; - private Submarine endOutpost = null; + private SubmarineInfo startOutpost = null; + private SubmarineInfo endOutpost = null; private bool currentTutorialCompleted = false; private float fadeOutTime = 3f; protected float waitBeforeFade = 4f; @@ -56,13 +56,13 @@ namespace Barotrauma.Tutorials private IEnumerable Loading() { - Submarine.MainSub = Submarine.Load(submarinePath, "", true); + SubmarineInfo subInfo = new SubmarineInfo(submarinePath); LevelGenerationParams generationParams = LevelGenerationParams.LevelParams.Find(p => p.Name == levelParams); yield return CoroutineStatus.Running; - GameMain.GameSession = new GameSession(Submarine.MainSub, "", + GameMain.GameSession = new GameSession(subInfo, "", GameModePreset.List.Find(g => g.Identifier == "tutorial")); (GameMain.GameSession.GameMode as TutorialMode).Tutorial = this; @@ -72,12 +72,12 @@ namespace Barotrauma.Tutorials if (startOutpostPath != string.Empty) { - startOutpost = Submarine.Load(startOutpostPath, "", false); + startOutpost = new SubmarineInfo(startOutpostPath); } if (endOutpostPath != string.Empty) { - endOutpost = Submarine.Load(endOutpostPath, "", false); + endOutpost = new SubmarineInfo(endOutpostPath); } Level tutorialLevel = new Level(levelSeed, 0, 0, generationParams, biome, startOutpost, endOutpost); @@ -160,11 +160,11 @@ namespace Barotrauma.Tutorials switch (this.spawnSub) { case "startoutpost": - spawnSub = startOutpost; + spawnSub = Level.Loaded.StartOutpost; break; case "endoutpost": - spawnSub = endOutpost; + spawnSub = Level.Loaded.EndOutpost; break; default: diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs index bbd67e90e..21fa67877 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/Tutorials/Tutorial.cs @@ -533,15 +533,15 @@ namespace Barotrauma.Tutorials titleBlock.RectTransform.IsFixedSize = true; } - List colorData = ColorData.GetColorData(text, out text); + List richTextData = RichTextData.GetRichTextData(text, out text); GUITextBlock textBlock; - if (colorData == null) + if (richTextData == null) { textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), " " + text, wrap: true); } else { - textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), colorData, " " + text, wrap: true); + textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), richTextData, " " + text, wrap: true); } textBlock.RectTransform.IsFixedSize = true; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs index 903f55d0f..38e4dff6d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameSession.cs @@ -1,159 +1,41 @@ -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; -using System.Collections.Generic; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; namespace Barotrauma { partial class GameSession { - private InfoFrameTab selectedTab; - private GUIFrame infoFrame; - - private readonly List tabButtons = new List(); - - private GUIFrame infoFrameContent; public RoundSummary RoundSummary { get; private set; } - public static bool IsInfoFrameOpen => GameMain.GameSession?.infoFrame != null; + public static bool IsTabMenuOpen => GameMain.GameSession?.tabMenu != null; + public static TabMenu TabMenuInstance => GameMain.GameSession?.tabMenu; - private bool ToggleInfoFrame() + private TabMenu tabMenu; + + public bool ToggleTabMenu() { if (GameMain.NetworkMember != null && GameMain.NetLobbyScreen != null) { if (GameMain.NetLobbyScreen.HeadSelectionList != null) { GameMain.NetLobbyScreen.HeadSelectionList.Visible = false; } if (GameMain.NetLobbyScreen.JobSelectionFrame != null) { GameMain.NetLobbyScreen.JobSelectionFrame.Visible = false; } } - if (infoFrame == null) + if (tabMenu == null && GameMode is TutorialMode == false) { - CreateInfoFrame(); - SelectInfoFrameTab(null, selectedTab); + tabMenu = new TabMenu(); } else { - infoFrame = null; + tabMenu = null; + NetLobbyScreen.JobInfoFrame = null; } return true; } - public void CreateInfoFrame() - { - int width = 600, height = 400; - - tabButtons.Clear(); - - infoFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), style: "GUIBackgroundBlocker"); - - var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.35f), infoFrame.RectTransform, Anchor.Center) { MinSize = new Point(width, height), RelativeOffset = new Vector2(0.0f, 0.033f) }); - - var paddedFrame = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.9f), innerFrame.RectTransform, Anchor.Center), style: null); - var buttonArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.08f), paddedFrame.RectTransform), isHorizontal: true) - { - RelativeSpacing = 0.01f - }; - infoFrameContent = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.85f), paddedFrame.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.08f) }, style: "InnerFrame"); - - var crewButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), buttonArea.RectTransform), TextManager.Get("Crew"), style: "GUITabButton") - { - UserData = InfoFrameTab.Crew, - OnClicked = SelectInfoFrameTab - }; - tabButtons.Add(crewButton); - - var missionButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), buttonArea.RectTransform), TextManager.Get("Mission"), style: "GUITabButton") - { - UserData = InfoFrameTab.Mission, - OnClicked = SelectInfoFrameTab - }; - tabButtons.Add(missionButton); - - if (GameMain.NetworkMember != null) - { - var myCharacterButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), buttonArea.RectTransform), TextManager.Get("MyCharacter"), style: "GUITabButton") - { - UserData = InfoFrameTab.MyCharacter, - OnClicked = SelectInfoFrameTab - }; - tabButtons.Add(myCharacterButton); - } - - /*TODO: fix - if (GameMain.Server != null) - { - var manageButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), buttonArea.RectTransform), TextManager.Get("ManagePlayers")) - { - UserData = InfoFrameTab.ManagePlayers, - OnClicked = SelectInfoFrameTab - }; - }*/ - - } - - private bool SelectInfoFrameTab(GUIButton button, object userData) - { - selectedTab = (InfoFrameTab)userData; - - CreateInfoFrame(); - tabButtons.ForEach(tb => tb.Selected = (InfoFrameTab)tb.UserData == selectedTab); - - switch (selectedTab) - { - case InfoFrameTab.Crew: - CrewManager.CreateCrewListFrame(CrewManager.GetCharacters(), infoFrameContent); - break; - case InfoFrameTab.Mission: - CreateMissionInfo(infoFrameContent); - break; - case InfoFrameTab.MyCharacter: - if (GameMain.NetworkMember == null) { return false; } - GameMain.NetLobbyScreen.CreatePlayerFrame(infoFrameContent); - break; - case InfoFrameTab.ManagePlayers: - //TODO: fix - //GameMain.Server.ManagePlayersFrame(infoFrameContent); - break; - } - - return true; - } - - private void CreateMissionInfo(GUIFrame infoFrame) - { - infoFrameContent.ClearChildren(); - - var isTraitor = GameMain.Client?.Character?.IsTraitor ?? false; - - var missionFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, isTraitor ? 0.95f : 0.45f), infoFrameContent.RectTransform)) - { - RelativeSpacing = 0.05f - }; - - if (Mission != null) - { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionFrame.RectTransform), Mission.Name, font: GUI.LargeFont); - - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionFrame.RectTransform), TextManager.GetWithVariable("MissionReward", "[reward]", Mission.Reward.ToString())); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionFrame.RectTransform), Mission.Description, wrap: true); - } - else - { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionFrame.RectTransform, Anchor.TopCenter), TextManager.Get("NoMission"), font: GUI.LargeFont); - } - if (isTraitor) - { - var traitorFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.45f), infoFrameContent.RectTransform, Anchor.BottomLeft)) - { - RelativeSpacing = 0.05f - }; - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), traitorFrame.RectTransform), TextManager.Get("Traitors"), font: GUI.LargeFont); - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), traitorFrame.RectTransform), GameMain.Client.Character.TraitorCurrentObjective, wrap: true); - } - } - public void AddToGUIUpdateList() { if (GUI.DisableHUD) return; GameMode?.AddToGUIUpdateList(); - infoFrame?.AddToGUIUpdateList(); + tabMenu?.AddToGUIUpdateList(); if (GameMain.NetworkMember != null) { @@ -166,17 +48,31 @@ namespace Barotrauma { if (GUI.DisableHUD) return; - if (PlayerInput.KeyDown(InputType.InfoTab) && - (GUI.KeyboardDispatcher.Subscriber == null || GUI.KeyboardDispatcher.Subscriber is GUIListBox)) + if (GameMode.IsRunning) { - if (infoFrame == null) + if (tabMenu == null) { - ToggleInfoFrame(); + if (PlayerInput.KeyHit(InputType.InfoTab) && GUI.KeyboardDispatcher.Subscriber is GUITextBox == false) + { + ToggleTabMenu(); + } + } + else + { + tabMenu.Update(); + + if (PlayerInput.KeyHit(InputType.InfoTab) && GUI.KeyboardDispatcher.Subscriber is GUITextBox == false) + { + ToggleTabMenu(); + } } } - else if (infoFrame != null) + else { - ToggleInfoFrame(); + if (tabMenu != null) + { + ToggleTabMenu(); + } } if (GameMain.NetworkMember != null) @@ -202,9 +98,7 @@ namespace Barotrauma public void Draw(SpriteBatch spriteBatch) { if (GUI.DisableHUD) return; - GameMode?.Draw(spriteBatch); - //infoFrame?.DrawManually(spriteBatch); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index 40a5ca72f..78b8f52fe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -24,7 +24,7 @@ namespace Barotrauma public GUIFrame CreateSummaryFrame(string endMessage) { bool singleplayer = GameMain.NetworkMember == null; - bool gameOver = gameSession.CrewManager.GetCharacters().All(c => c.IsDead || c.IsUnconscious); + bool gameOver = gameSession.CrewManager.GetCharacters().All(c => c.IsDead || c.IsIncapacitated); bool progress = Submarine.MainSub.AtEndPosition; if (!singleplayer) { @@ -55,7 +55,7 @@ namespace Barotrauma string summaryText = TextManager.GetWithVariables(gameOver ? "RoundSummaryGameOver" : (progress ? "RoundSummaryProgress" : "RoundSummaryReturn"), new string[2] { "[sub]", "[location]" }, - new string[2] { Submarine.MainSub.Name, progress ? GameMain.GameSession.EndLocation.Name : GameMain.GameSession.StartLocation.Name }); + new string[2] { Submarine.MainSub.Info.Name, progress ? GameMain.GameSession.EndLocation.Name : GameMain.GameSession.StartLocation.Name }); var infoText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoTextBox.Content.RectTransform), summaryText, wrap: true); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs index 1c636b45c..b88bcafa0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSettings.cs @@ -18,6 +18,7 @@ namespace Barotrauma { Graphics, Audio, + VoiceChat, Controls, #if DEBUG Debug @@ -60,6 +61,7 @@ namespace Barotrauma keyMapping[(int)InputType.CrewOrders] = new KeyOrMouse(Keys.C); keyMapping[(int)InputType.Voice] = new KeyOrMouse(Keys.V); + keyMapping[(int)InputType.LocalVoice] = new KeyOrMouse(Keys.B); keyMapping[(int)InputType.Command] = new KeyOrMouse(MouseButton.MiddleMouse); if (Language == "French") @@ -443,11 +445,13 @@ namespace Barotrauma UserData = tab }; + float tabWidth = 0.25f; #if DEBUG + tabWidth = 0.2f; if (tab != Tab.Debug) { #endif - tabButtons[(int)tab] = new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), tabButtonHolder.RectTransform), + tabButtons[(int)tab] = new GUIButton(new RectTransform(new Vector2(tabWidth, 1.0f), tabButtonHolder.RectTransform), TextManager.Get("SettingsTab." + tab.ToString()), style: "GUITabButton") { UserData = tab, @@ -457,7 +461,7 @@ namespace Barotrauma } else { - tabButtons[(int)tab] = new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), tabButtonHolder.RectTransform), "Debug", style: "GUITabButton") + tabButtons[(int)tab] = new GUIButton(new RectTransform(new Vector2(tabWidth, 1.0f), tabButtonHolder.RectTransform), "Debug", style: "GUITabButton") { UserData = tab, OnClicked = (bt, userdata) => { SelectTab((Tab)userdata); return true; } @@ -677,10 +681,10 @@ namespace Barotrauma var audioContent = new GUILayoutGroup(new RectTransform(new Vector2(0.97f, 0.97f), tabs[(int)Tab.Audio].RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) { - Stretch = true, + Stretch = false, RelativeSpacing = 0.01f }; - + GUITextBlock soundVolumeText = new GUITextBlock(new RectTransform(textBlockScale, audioContent.RectTransform), TextManager.Get("SoundVolume"), font: GUI.SubHeadingFont); GUIScrollBar soundScrollBar = new GUIScrollBar(new RectTransform(textBlockScale, audioContent.RectTransform), style: "GUISlider", barSize: 0.05f) @@ -718,15 +722,16 @@ namespace Barotrauma style: "GUISlider", barSize: 0.05f) { UserData = voiceChatVolumeText, - BarScroll = VoiceChatVolume, - OnMoved = (scrollBar, scroll) => - { - ChangeSliderText(scrollBar, scroll); - VoiceChatVolume = scroll; - return true; - }, + Range = new Vector2(0.0f, 2.0f), Step = 0.05f }; + voiceChatScrollBar.BarScrollValue = VoiceChatVolume; + voiceChatScrollBar.OnMoved = (scrollBar, scroll) => + { + ChangeSliderText(scrollBar, scrollBar.BarScrollValue); + VoiceChatVolume = scrollBar.BarScrollValue; + return true; + }; voiceChatScrollBar.OnMoved(voiceChatScrollBar, voiceChatScrollBar.BarScroll); GUITickBox muteOnFocusLostBox = new GUITickBox(new RectTransform(tickBoxScale, audioContent.RectTransform), TextManager.Get("MuteOnFocusLost")) @@ -765,7 +770,15 @@ namespace Barotrauma } }; - new GUITextBlock(new RectTransform(textBlockScale, audioContent.RectTransform), TextManager.Get("VoiceChat"), font: GUI.SubHeadingFont); + /// Voice chat tab ---------------------------------------------------------------- + + var voiceChatContent = new GUILayoutGroup(new RectTransform(new Vector2(0.97f, 0.97f), tabs[(int)Tab.VoiceChat].RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter) + { + Stretch = false, + RelativeSpacing = 0.01f + }; + + //new GUITextBlock(new RectTransform(textBlockScale, voiceChatContent.RectTransform), TextManager.Get("VoiceChat"), font: GUI.SubHeadingFont); CaptureDeviceNames = Alc.GetStringList((IntPtr)null, Alc.CaptureDeviceSpecifier); foreach (string name in CaptureDeviceNames) @@ -773,7 +786,7 @@ namespace Barotrauma DebugConsole.NewMessage(name + " " + name.Length.ToString(), Color.Lime); } - GUITickBox directionalVoiceChat = new GUITickBox(new RectTransform(tickBoxScale, audioContent.RectTransform), TextManager.Get("DirectionalVoiceChat")) + GUITickBox directionalVoiceChat = new GUITickBox(new RectTransform(tickBoxScale, voiceChatContent.RectTransform), TextManager.Get("DirectionalVoiceChat")) { Selected = UseDirectionalVoiceChat, ToolTip = TextManager.Get("DirectionalVoiceChatToolTip"), @@ -794,7 +807,7 @@ namespace Barotrauma VoiceSetting = VoiceMode.Disabled; } #if (!OSX) - var deviceList = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.15f), audioContent.RectTransform), TrimAudioDeviceName(VoiceCaptureDevice), CaptureDeviceNames.Count); + var deviceList = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.15f), voiceChatContent.RectTransform), TrimAudioDeviceName(VoiceCaptureDevice), CaptureDeviceNames.Count); if (CaptureDeviceNames?.Count > 0) { foreach (string name in CaptureDeviceNames) @@ -819,7 +832,7 @@ namespace Barotrauma } #else - var defaultDeviceGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.3f), audioContent.RectTransform), true, Anchor.CenterLeft); + var defaultDeviceGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.3f), voiceChatContent.RectTransform), true, Anchor.CenterLeft); var currentDeviceTextBlock = new GUITextBlock(new RectTransform(new Vector2(.7f, 0.75f), null), TextManager.AddPunctuation(':', TextManager.Get("CurrentDevice"), TrimAudioDeviceName(VoiceCaptureDevice)), font: GUI.SubHeadingFont) { @@ -857,15 +870,15 @@ namespace Barotrauma #endif var voiceModeCount = Enum.GetNames(typeof(VoiceMode)).Length; - var voiceModeDropDown = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.15f), audioContent.RectTransform), elementCount: voiceModeCount); + var voiceModeDropDown = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.15f), voiceChatContent.RectTransform), elementCount: voiceModeCount); for (int i = 0; i < voiceModeCount; i++) { var voiceMode = "VoiceMode." + ((VoiceMode)i).ToString(); voiceModeDropDown.AddItem(TextManager.Get(voiceMode), userData: i, toolTip: TextManager.Get(voiceMode + "ToolTip")); } - var micVolumeText = new GUITextBlock(new RectTransform(textBlockScale, audioContent.RectTransform), TextManager.Get("MicrophoneVolume"), font: GUI.SubHeadingFont); - var micVolumeSlider = new GUIScrollBar(new RectTransform(textBlockScale, audioContent.RectTransform), + var micVolumeText = new GUITextBlock(new RectTransform(textBlockScale, voiceChatContent.RectTransform), TextManager.Get("MicrophoneVolume"), font: GUI.SubHeadingFont); + var micVolumeSlider = new GUIScrollBar(new RectTransform(textBlockScale, voiceChatContent.RectTransform), style: "GUISlider", barSize: 0.05f) { UserData = micVolumeText, @@ -882,7 +895,7 @@ namespace Barotrauma }; micVolumeSlider.OnMoved(micVolumeSlider, micVolumeSlider.BarScroll); - var extraVoiceSettingsContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.25f), audioContent.RectTransform, Anchor.BottomCenter), style: null); + var extraVoiceSettingsContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), voiceChatContent.RectTransform, Anchor.BottomCenter), style: null); var voiceActivityGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), extraVoiceSettingsContainer.RectTransform)) { @@ -928,8 +941,8 @@ namespace Barotrauma return true; }; - var voiceInputContainer = new GUILayoutGroup( - new RectTransform(new Vector2(1.0f, 0.25f), extraVoiceSettingsContainer.RectTransform) + var voiceInputContainerHorizontal = new GUILayoutGroup( + new RectTransform(new Vector2(1.0f, 0.5f), extraVoiceSettingsContainer.RectTransform) { RelativeOffset = new Vector2(0.0f, voiceActivityGroup.RectTransform.RelativeSize.Y + 0.1f) }, @@ -937,6 +950,11 @@ namespace Barotrauma { Visible = VoiceSetting == VoiceMode.PushToTalk }; + + var voiceInputContainer = new GUILayoutGroup( + new RectTransform(new Vector2(0.5f, 1.0f), voiceInputContainerHorizontal.RectTransform), + isHorizontal: true, childAnchor: Anchor.CenterLeft); + new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), voiceInputContainer.RectTransform), TextManager.Get("InputType.Voice"), font: GUI.SubHeadingFont); var voiceKeyBox = new GUITextBox(new RectTransform(new Vector2(0.4f, 1.0f), voiceInputContainer.RectTransform, Anchor.TopRight), text: KeyBindText(InputType.Voice)) { @@ -945,6 +963,39 @@ namespace Barotrauma }; voiceKeyBox.OnSelected += KeyBoxSelected; + var localVoiceInputContainer = new GUILayoutGroup( + new RectTransform(new Vector2(0.5f, 1.0f), voiceInputContainerHorizontal.RectTransform), + isHorizontal: true, childAnchor: Anchor.CenterLeft); + + new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), localVoiceInputContainer.RectTransform), TextManager.Get("InputType.LocalVoice"), font: GUI.SubHeadingFont); + var localVoiceKeyBox = new GUITextBox(new RectTransform(new Vector2(0.4f, 1.0f), localVoiceInputContainer.RectTransform, Anchor.TopRight), text: KeyBindText(InputType.LocalVoice)) + { + SelectedColor = Color.Gold * 0.3f, + UserData = InputType.LocalVoice + }; + localVoiceKeyBox.OnSelected += KeyBoxSelected; + + var cutoffPreventionText = new GUITextBlock(new RectTransform(textBlockScale, voiceChatContent.RectTransform), TextManager.Get("CutoffPrevention"), font: GUI.SubHeadingFont) + { + ToolTip = TextManager.Get("CutoffPreventionTooltip") + }; + var cutoffPreventionSlider = new GUIScrollBar(new RectTransform(textBlockScale, voiceChatContent.RectTransform), + style: "GUISlider", barSize: 0.05f) + { + UserData = micVolumeText, + Range = new Vector2(0,540), + Step = 1.0f / 9.0f + }; + cutoffPreventionSlider.BarScrollValue = VoiceChatCutoffPrevention; + cutoffPreventionSlider.OnMoved = (scrollBar, scroll) => + { + VoiceChatCutoffPrevention = (int)scrollBar.BarScrollValue; + cutoffPreventionText.Text = TextManager.Get("CutoffPrevention") + + " " + TextManager.GetWithVariable("timeformatmilliseconds", "[milliseconds]", VoiceChatCutoffPrevention.ToString()); + return true; + }; + cutoffPreventionSlider.OnMoved(cutoffPreventionSlider, cutoffPreventionSlider.BarScrollValue); + voiceModeDropDown.OnSelected = (GUIComponent selected, object userData) => { try @@ -961,7 +1012,7 @@ namespace Barotrauma { VoiceSetting = vMode = VoiceMode.Disabled; voiceActivityGroup.Visible = false; - voiceInputContainer.Visible = false; + voiceInputContainerHorizontal.Visible = false; return true; } } @@ -977,7 +1028,7 @@ namespace Barotrauma noiseGateText.Visible = (vMode == VoiceMode.Activity); noiseGateSlider.Visible = (vMode == VoiceMode.Activity); voiceActivityGroup.Visible = (vMode != VoiceMode.Disabled); - voiceInputContainer.Visible = (vMode == VoiceMode.PushToTalk); + voiceInputContainerHorizontal.Visible = (vMode == VoiceMode.PushToTalk); UnsavedSettings = true; } catch (Exception e) @@ -1182,7 +1233,7 @@ namespace Barotrauma { RelativeOffset = new Vector2(0.02f, 0.02f) }) { RelativeSpacing = 0.01f }; - var automaticQuickStartTickBox = new GUITickBox(new RectTransform(tickBoxScale / 0.18f, debugTickBoxes.RectTransform, scaleBasis: ScaleBasis.BothHeight), "Enable automatic quickstart", style: "GUITickBox"); + var automaticQuickStartTickBox = new GUITickBox(new RectTransform(tickBoxScale / 0.18f, debugTickBoxes.RectTransform, scaleBasis: ScaleBasis.BothHeight), "Automatic quickstart enabled", style: "GUITickBox"); automaticQuickStartTickBox.Selected = AutomaticQuickStartEnabled; automaticQuickStartTickBox.ToolTip = "Will the game automatically move on to Quickstart when the game is launched"; automaticQuickStartTickBox.OnSelected = (tickBox) => @@ -1192,7 +1243,7 @@ namespace Barotrauma return true; }; - var showSplashScreenTickBox = new GUITickBox(new RectTransform(tickBoxScale / 0.18f, debugTickBoxes.RectTransform, scaleBasis: ScaleBasis.BothHeight), "Show splash screen", style: "GUITickBox"); + var showSplashScreenTickBox = new GUITickBox(new RectTransform(tickBoxScale / 0.18f, debugTickBoxes.RectTransform, scaleBasis: ScaleBasis.BothHeight), "Splash screen enabled", style: "GUITickBox"); showSplashScreenTickBox.Selected = EnableSplashScreen; showSplashScreenTickBox.ToolTip = "Are the splash screens shown when the game is launched"; showSplashScreenTickBox.OnSelected = (tickBox) => @@ -1202,7 +1253,7 @@ namespace Barotrauma return true; }; - var verboseLoggingTickBox = new GUITickBox(new RectTransform(tickBoxScale / 0.18f, debugTickBoxes.RectTransform, scaleBasis: ScaleBasis.BothHeight), "Enable verbose logging", style: "GUITickBox"); + var verboseLoggingTickBox = new GUITickBox(new RectTransform(tickBoxScale / 0.18f, debugTickBoxes.RectTransform, scaleBasis: ScaleBasis.BothHeight), "Verbose logging enabled", style: "GUITickBox"); verboseLoggingTickBox.Selected = VerboseLogging; verboseLoggingTickBox.ToolTip = "Should verbose logging be used"; verboseLoggingTickBox.OnSelected = (tickBox) => @@ -1211,6 +1262,16 @@ namespace Barotrauma UnsavedSettings = true; return true; }; + + var textManagerDebugModeTickBox = new GUITickBox(new RectTransform(tickBoxScale / 0.18f, debugTickBoxes.RectTransform, scaleBasis: ScaleBasis.BothHeight), "TextManager debug mode enabled", style: "GUITickBox"); + textManagerDebugModeTickBox.Selected = TextManagerDebugModeEnabled; + textManagerDebugModeTickBox.ToolTip = "Does the TextManager return the text tags for debug purposes?"; + textManagerDebugModeTickBox.OnSelected = (tickBox) => + { + TextManagerDebugModeEnabled = tickBox.Selected; + UnsavedSettings = true; + return true; + }; #endif UnsavedSettings = false; // Reset unsaved settings to false once the UI has been created @@ -1279,7 +1340,7 @@ namespace Barotrauma string[] prefixes = { "OpenAL Soft on " }; foreach (string prefix in prefixes) { - if (name.StartsWith(prefix, StringComparison.InvariantCulture)) + if (name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { return name.Remove(0, prefix.Length); } @@ -1292,7 +1353,7 @@ namespace Barotrauma { switch (tab) { - case Tab.Audio: + case Tab.VoiceChat: if (VoiceSetting != VoiceMode.Disabled) { if (GameMain.Client == null && VoipCapture.Instance == null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index a910bbb99..3ce961f69 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -43,6 +43,7 @@ namespace Barotrauma public Vector2[] SlotPositions; public static Point SlotSize; public static int Spacing; + public static int HideButtonWidth; private Layout layout; public Layout CurrentLayout @@ -71,22 +72,44 @@ namespace Barotrauma { get { return personalSlotArea; } } - + + private GUIImage[] indicators = new GUIImage[5]; + private int[] indicatorIndexes = new int[5]; + private Vector2 indicatorSpriteSize; + private GUILayoutGroup indicatorGroup; + partial void InitProjSpecific(XElement element) { Hidden = true; - hideButton = new GUIButton(new RectTransform(new Point((int)(30 * GUI.Scale), (int)(60 * GUI.Scale)), GUI.Canvas) + hideButton = new GUIButton(new RectTransform(new Point((int)(31f * (HUDLayoutSettings.BottomRightInfoArea.Height / 100f)), HUDLayoutSettings.BottomRightInfoArea.Height), GUI.Canvas) { AbsoluteOffset = HUDLayoutSettings.CrewArea.Location }, - "", style: "UIToggleButton"); - hideButton.Children.ForEach(c => c.SpriteEffects = SpriteEffects.FlipHorizontally); + "", style: "EquipmentToggleButton"); + + indicatorGroup = new GUILayoutGroup(new RectTransform(Point.Zero, hideButton.RectTransform)) { IsHorizontal = false }; + indicatorGroup.ChildAnchor = Anchor.TopCenter; + indicatorSpriteSize = GUI.Style.GetComponentStyle("EquipmentIndicatorDivingSuit").Sprites[GUIComponent.ComponentState.None][0].Sprite.size; + + indicators[0] = new GUIImage(new RectTransform(Point.Zero, indicatorGroup.RectTransform), "EquipmentIndicatorDivingSuit"); + indicators[1] = new GUIImage(new RectTransform(Point.Zero, indicatorGroup.RectTransform), "EquipmentIndicatorID"); + indicators[2] = new GUIImage(new RectTransform(Point.Zero, indicatorGroup.RectTransform), "EquipmentIndicatorOutfit"); + indicators[3] = new GUIImage(new RectTransform(Point.Zero, indicatorGroup.RectTransform), "EquipmentIndicatorHeadwear"); + indicators[4] = new GUIImage(new RectTransform(Point.Zero, indicatorGroup.RectTransform), "EquipmentIndicatorHeadphones"); + + indicatorIndexes[0] = FindLimbSlot(InvSlotType.OuterClothes); + indicatorIndexes[1] = FindLimbSlot(InvSlotType.Card); + indicatorIndexes[2] = FindLimbSlot(InvSlotType.InnerClothes); + indicatorIndexes[3] = FindLimbSlot(InvSlotType.Head); + indicatorIndexes[4] = FindLimbSlot(InvSlotType.Headset); + + for (int i = 0; i < indicators.Length; i++) + { + indicators[i].CanBeFocused = false; + } + hideButton.OnClicked += (GUIButton btn, object userdata) => { hidePersonalSlots = !hidePersonalSlots; - foreach (GUIComponent child in btn.Children) - { - child.SpriteEffects = hidePersonalSlots ? SpriteEffects.None : SpriteEffects.FlipHorizontally; - } return true; }; @@ -245,6 +268,26 @@ namespace Barotrauma return false; } + + private void SetIndicatorSizes() + { + indicatorGroup.RectTransform.AbsoluteOffset = new Point((int)Math.Round(4 * GUI.Scale), (int)Math.Round(7 * GUI.Scale)); + indicatorGroup.RectTransform.NonScaledSize = new Point(hideButton.Rect.Width - indicatorGroup.RectTransform.AbsoluteOffset.X * 2, hideButton.Rect.Height - indicatorGroup.RectTransform.AbsoluteOffset.Y * 2); + indicatorGroup.AbsoluteSpacing = (int)Math.Ceiling(2 * GUI.Scale); + + int indicatorHeight = (indicatorGroup.RectTransform.NonScaledSize.Y - indicatorGroup.AbsoluteSpacing * (indicators.Length - 1)) / indicators.Length; + int indicatorWidth = (int)(indicatorSpriteSize.X / (indicatorSpriteSize.Y / indicatorHeight)); + + if (HideButtonWidth % 2 != indicatorWidth % 2) indicatorWidth++; + + Point indicatorSize = new Point(indicatorWidth, indicatorHeight); + + for (int i = 0; i < indicators.Length; i++) + { + indicators[i].RectTransform.NonScaledSize = indicatorSize; + } + } + private void SetSlotPositions(Layout layout) { bool isFourByThree = GUI.IsFourByThree(); @@ -257,6 +300,8 @@ namespace Barotrauma Spacing = (int)(8 * UIScale); } + HideButtonWidth = (int)(31f * (HUDLayoutSettings.BottomRightInfoArea.Height / 100f)); + SlotSize = !isFourByThree ? (SlotSpriteSmall.size * UIScale).ToPoint() : (SlotSpriteSmall.size * UIScale * .925f).ToPoint(); int bottomOffset = SlotSize.Y + Spacing * 2 + ContainedIndicatorHeight; @@ -272,8 +317,7 @@ namespace Barotrauma int normalSlotCount = SlotTypes.Count(s => !PersonalSlots.HasFlag(s)); int x = GameMain.GraphicsWidth / 2 - normalSlotCount * (SlotSize.X + Spacing) / 2; - int upperX = HUDLayoutSettings.BottomRightInfoArea.X - Spacing * 2 - SlotSize.X - SlotSize.X / 2; - //int upperX = GameMain.GraphicsWidth - personalSlotCount * (slotSize.X + spacing) + (int)(11 * GUI.Scale) + spacing; + int upperX = HUDLayoutSettings.BottomRightInfoArea.X - SlotSize.X - Spacing * 4 - HideButtonWidth; //make sure the rightmost normal slot doesn't overlap with the personal slots x -= Math.Max((x + normalSlotCount * (SlotSize.X + Spacing)) - (upperX - personalSlotCount * (SlotSize.X + Spacing)), 0); @@ -300,11 +344,11 @@ namespace Barotrauma if (hideButtonSlotIndex > -1) { hideButton.RectTransform.SetPosition(Anchor.TopLeft, Pivot.TopLeft); - hideButton.RectTransform.NonScaledSize = new Point(SlotSize.X / 2, HUDLayoutSettings.BottomRightInfoArea.Height); - hideButton.RectTransform.AbsoluteOffset = new Point( - personalSlotArea.Right + Spacing, - HUDLayoutSettings.BottomRightInfoArea.Y); + hideButton.RectTransform.NonScaledSize = new Point(HideButtonWidth, HUDLayoutSettings.BottomRightInfoArea.Height); + hideButton.RectTransform.AbsoluteOffset = new Point(HUDLayoutSettings.BottomRightInfoArea.Left - HideButtonWidth + GUI.IntScaleCeiling(2f), HUDLayoutSettings.BottomRightInfoArea.Y + GUI.IntScaleCeiling(1f)); hideButton.Visible = true; + + SetIndicatorSizes(); } } break; @@ -447,7 +491,7 @@ namespace Barotrauma ((selectedSlot != null && selectedSlot.IsSubSlot) || (draggingItem != null && (draggingSlot == null || !draggingSlot.MouseOn()))); if (CharacterHealth.OpenHealthWindow != null) hoverOnInventory = true; - if (layout == Layout.Default) + if (layout == Layout.Default && (Screen.Selected != GameMain.SubEditorScreen || Screen.Selected is SubEditorScreen editor && editor.WiringMode)) { if (hideButton.Visible) { @@ -457,7 +501,8 @@ namespace Barotrauma hidePersonalSlotsState = hidePersonalSlots ? Math.Min(hidePersonalSlotsState + deltaTime * 5.0f, 1.0f) : Math.Max(hidePersonalSlotsState - deltaTime * 5.0f, 0.0f); - + + bool personalSlotsMoving = hidePersonalSlotsState > 0 && hidePersonalSlotsState < 1f; for (int i = 0; i < slots.Length; i++) { if (!PersonalSlots.HasFlag(SlotTypes[i])) { continue; } @@ -466,6 +511,7 @@ namespace Barotrauma if (selectedSlot?.Slot == slots[i]) { selectedSlot = null; } highlightedSubInventorySlots.RemoveWhere(s => s.Slot == slots[i]); } + slots[i].IsMoving = personalSlotsMoving; slots[i].DrawOffset = Vector2.Lerp(Vector2.Zero, new Vector2(personalSlotArea.Width, 0.0f), hidePersonalSlotsState); } } @@ -538,6 +584,19 @@ namespace Barotrauma } } } + + // In sub editor we cannot hover over the slot because they are not rendered so we override it here + if (Screen.Selected is SubEditorScreen subEditor && !subEditor.WiringMode) + { + for (int i = 0; i < slots.Length; i++) + { + var subInventory = GetSubInventory(i); + if (subInventory != null) + { + ShowSubInventory(new SlotReference(this, slots[i], i, false, Items[i].GetComponent().Inventory), deltaTime, cam, hideSubInventories, true); + } + } + } foreach (var subInventorySlot in hideSubInventories) { @@ -551,6 +610,8 @@ namespace Barotrauma if (character == Character.Controlled && character.SelectedCharacter == null) // Permanently open subinventories only available when the default UI layout is in use -> not when grabbing characters { + UpdateEquipmentIndicators(); + //remove the highlighted slots of other characters' inventories when not grabbing anyone highlightedSubInventorySlots.RemoveWhere(s => s.ParentInventory != this && s.ParentInventory?.Owner is Character); @@ -651,6 +712,39 @@ namespace Barotrauma } } } + + private void UpdateEquipmentIndicators() + { + for (int i = 0; i < indicators.Length; i++) + { + Item item = Items[indicatorIndexes[i]]; + if (item != null) + { + Wearable wearable = item.GetComponent(); + if (wearable != null && wearable.DisplayContainedStatus) + { + float conditionPercentage = item.GetContainedItemConditionPercentage(); + + if (conditionPercentage != -1) + { + indicators[i].Color = ToolBox.GradientLerp(conditionPercentage, GUI.Style.EquipmentIndicatorRunningOut, GUI.Style.EquipmentIndicatorEquipped); + } + else + { + indicators[i].Color = GUI.Style.EquipmentIndicatorRunningOut; + } + } + else + { + indicators[i].Color = GUI.Style.EquipmentIndicatorEquipped; + } + } + else + { + indicators[i].Color = GUI.Style.EquipmentIndicatorNotEquipped; + } + } + } private void ShowSubInventory(SlotReference slotRef, float deltaTime, Camera cam, List hideSubInventories, bool isEquippedSubInventory) { @@ -771,7 +865,7 @@ namespace Barotrauma { return QuickUseAction.TakeFromCharacter; } - else if (character.SelectedItems.Any(i => i?.OwnInventory != null && i.OwnInventory.CanBePut(item))) + else if (character.SelectedItems.Any(i => i?.OwnInventory != null && i.OwnInventory.CanBePut(item)) && allowInventorySwap) { return QuickUseAction.PutToEquippedItem; } @@ -799,13 +893,40 @@ namespace Barotrauma private void QuickUseItem(Item item, bool allowEquip, bool allowInventorySwap, bool allowApplyTreatment) { + if (Screen.Selected is SubEditorScreen editor && !editor.WiringMode && !Submarine.Unloading) + { + // Find the slot the item was contained in and flash it + if (item.ParentInventory?.slots != null) + { + var invSlots = item.ParentInventory.slots; + var invItems = item.ParentInventory.Items; + for (int i = 0; i < invSlots.Length; i++) + { + if (i < 0 || invSlots.Length <= i || i < 0 || invItems.Length <= i) { break; } + + var slot = invSlots[i]; + var slotItem = invItems[i]; + + if (slotItem == item) + { + slot.ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.4f); + GUI.PlayUISound(GUISoundType.PickItem); + break; + } + } + } + + item.Remove(); + return; + } + var quickUseAction = GetQuickUseAction(item, allowEquip, allowInventorySwap, allowApplyTreatment); bool success = false; switch (quickUseAction) { case QuickUseAction.Equip: //attempt to put in a free slot first - for (int i = 0; i < capacity; i++) + for (int i = capacity - 1; i >= 0; i--) { if (Items[i] != null) continue; if (SlotTypes[i] == InvSlotType.Any || !item.AllowedSlots.Any(a => a.HasFlag(SlotTypes[i]))) continue; @@ -815,7 +936,7 @@ namespace Barotrauma if (!success) { - for (int i = 0; i < capacity; i++) + for (int i = capacity - 1; i >= 0; i--) { if (SlotTypes[i] == InvSlotType.Any || !item.AllowedSlots.Any(a => a.HasFlag(SlotTypes[i]))) continue; //something else already equipped in the slot, attempt to unequip it @@ -964,7 +1085,7 @@ namespace Barotrauma if (limbSlotIcons.ContainsKey(SlotTypes[i])) { var icon = limbSlotIcons[SlotTypes[i]]; - icon.Draw(spriteBatch, slots[i].Rect.Center.ToVector2() + slots[i].DrawOffset, GUIColorSettings.EquipmentSlotIconColor, origin: icon.size / 2, scale: slots[i].Rect.Width / icon.size.X); + icon.Draw(spriteBatch, slots[i].Rect.Center.ToVector2() + slots[i].DrawOffset, GUI.Style.EquipmentSlotIconColor, origin: icon.size / 2, scale: slots[i].Rect.Width / icon.size.X); } continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs index 7ec738285..9a76f7419 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ItemComponent.cs @@ -52,10 +52,14 @@ namespace Barotrauma.Items.Components get { return sounds.Count > 0; } } - private bool[] hasSoundsOfType; - private Dictionary> sounds; + private readonly bool[] hasSoundsOfType; + private readonly Dictionary> sounds; private Dictionary soundSelectionModes; + protected float correctionTimer; + + public float IsActiveTimer; + public GUILayoutSettings DefaultLayout { get; protected set; } public GUILayoutSettings AlternativeLayout { get; protected set; } @@ -230,20 +234,23 @@ namespace Barotrauma.Items.Components if (loopingSound != null) { - float targetGain = 0.0f; if (Vector3.DistanceSquared(GameMain.SoundManager.ListenerPosition, new Vector3(item.WorldPosition, 0.0f)) > loopingSound.Range * loopingSound.Range || - (targetGain = GetSoundVolume(loopingSound)) <= 0.0001f) + (GetSoundVolume(loopingSound)) <= 0.0001f) { if (loopingSoundChannel != null) { - loopingSoundChannel.FadeOutAndDispose(); loopingSoundChannel = null; + loopingSoundChannel.FadeOutAndDispose(); + loopingSoundChannel = null; + loopingSound = null; } return; } if (loopingSoundChannel != null && loopingSoundChannel.Sound != loopingSound.RoundSound.Sound) { - loopingSoundChannel.FadeOutAndDispose(); loopingSoundChannel = null; + loopingSoundChannel.FadeOutAndDispose(); + loopingSoundChannel = null; + loopingSound = null; } if (loopingSoundChannel == null || !loopingSoundChannel.IsPlaying) { @@ -258,8 +265,7 @@ namespace Barotrauma.Items.Components } return; } - - ItemSound itemSound = null; + var matchingSounds = sounds[type]; if (loopingSoundChannel == null || !loopingSoundChannel.IsPlaying) { @@ -277,7 +283,7 @@ namespace Barotrauma.Items.Components { foreach (ItemSound sound in matchingSounds) { - PlaySound(sound, item.WorldPosition, user); + PlaySound(sound, item.WorldPosition); } return; } @@ -286,13 +292,12 @@ namespace Barotrauma.Items.Components index = Rand.Int(matchingSounds.Count); } - itemSound = matchingSounds[index]; - PlaySound(matchingSounds[index], item.WorldPosition, user); + PlaySound(matchingSounds[index], item.WorldPosition); } } - private void PlaySound(ItemSound itemSound, Vector2 position, Character user = null) + private void PlaySound(ItemSound itemSound, Vector2 position) { if (Vector2.DistanceSquared(new Vector2(GameMain.SoundManager.ListenerPosition.X, GameMain.SoundManager.ListenerPosition.Y), position) > itemSound.Range * itemSound.Range) { @@ -301,8 +306,7 @@ namespace Barotrauma.Items.Components if (itemSound.Loop) { - loopingSound = itemSound; - if (loopingSoundChannel != null && loopingSoundChannel.Sound != loopingSound.RoundSound.Sound) + if (loopingSoundChannel != null && loopingSoundChannel.Sound != itemSound.RoundSound.Sound) { loopingSoundChannel.FadeOutAndDispose(); loopingSoundChannel = null; } @@ -310,6 +314,7 @@ namespace Barotrauma.Items.Components { float volume = GetSoundVolume(itemSound); if (volume <= 0.0001f) { return; } + loopingSound = itemSound; loopingSoundChannel = loopingSound.RoundSound.Sound.Play( new Vector3(position.X, position.Y, 0.0f), 0.01f, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs index 317e98266..51a3a14fd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/LightComponent.cs @@ -3,11 +3,17 @@ using Barotrauma.Lights; using Barotrauma.Networking; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using System.Collections.Generic; namespace Barotrauma.Items.Components { partial class LightComponent : Powered, IServerSerializable, IDrawableComponent { + private bool? lastReceivedState; + + private CoroutineHandle resetPredictionCoroutine; + private float resetPredictionTimer; + public Vector2 DrawSize { get { return new Vector2(light.Range * 2, light.Range * 2); } @@ -32,6 +38,12 @@ namespace Barotrauma.Items.Components light.Color = LightColor.Multiply(brightness); } + public override void OnItemLoaded() + { + base.OnItemLoaded(); + SetLightSourceState(IsActive, lightBrightness); + } + public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) { if (light.LightSprite != null && (item.body == null || item.body.Enabled) && lightBrightness > 0.0f && IsOn) @@ -49,9 +61,36 @@ namespace Barotrauma.Items.Components } } + partial void OnStateChanged() + { + if (GameMain.Client == null || !lastReceivedState.HasValue) { return; } + //reset to last known server state after the state hasn't changed in 1.0 seconds client-side + resetPredictionTimer = 1.0f; + if (resetPredictionCoroutine == null || !CoroutineManager.IsCoroutineRunning(resetPredictionCoroutine)) + { + resetPredictionCoroutine = CoroutineManager.StartCoroutine(ResetPredictionAfterDelay()); + } + } + + /// + /// Reset client-side prediction of the light's state to the last known state sent by the server after resetPredictionTimer runs out + /// + private IEnumerable ResetPredictionAfterDelay() + { + while (resetPredictionTimer > 0.0f) + { + resetPredictionTimer -= CoroutineManager.DeltaTime; + yield return CoroutineStatus.Running; + } + if (lastReceivedState.HasValue) { IsActive = lastReceivedState.Value; } + resetPredictionCoroutine = null; + yield return CoroutineStatus.Success; + } + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { - IsOn = msg.ReadBoolean(); + IsActive = msg.ReadBoolean(); + lastReceivedState = IsActive; } protected override void RemoveComponentSpecific() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs index 098dd0abd..89165ecc0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs @@ -140,11 +140,11 @@ namespace Barotrauma.Items.Components var inputArea = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 1f), bottomFrame.RectTransform, Anchor.BottomCenter), isHorizontal: true, childAnchor: Anchor.BottomLeft); // === INPUT SLOTS === // - inputInventoryHolder = new GUIFrame(new RectTransform(new Vector2(0.8f, 1f), inputArea.RectTransform), style: null); + inputInventoryHolder = new GUIFrame(new RectTransform(new Vector2(0.7f, 1f), inputArea.RectTransform), style: null); new GUICustomComponent(new RectTransform(Vector2.One, inputInventoryHolder.RectTransform), DrawInputOverLay) { CanBeFocused = false }; // === ACTIVATE BUTTON === // - var buttonFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.2f, 0.8f), inputArea.RectTransform), childAnchor: Anchor.CenterRight); + var buttonFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 0.8f), inputArea.RectTransform), childAnchor: Anchor.CenterRight); activateButton = new GUIButton(new RectTransform(new Vector2(1f, 0.6f), buttonFrame.RectTransform), TextManager.Get("FabricatorCreate"), style: "DeviceButton") { @@ -356,6 +356,8 @@ namespace Barotrauma.Items.Components private void DrawOutputOverLay(SpriteBatch spriteBatch, GUICustomComponent overlayComponent) { overlayComponent.RectTransform.SetAsLastChild(); + + if (outputContainer.Inventory.Items.First() != null) { return; } FabricationRecipe targetItem = fabricatedItem ?? selectedItem; if (targetItem != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index 746fef710..b80dd19d0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -91,6 +91,7 @@ namespace Barotrauma.Items.Components if ((item.Submarine == null && displayedSubs.Count > 0) || //item not inside a sub anymore, but display is still showing subs !displayedSubs.Contains(item.Submarine) || //current sub not displayer item.Submarine.DockedTo.Any(s => !displayedSubs.Contains(s)) || //some of the docked subs not diplayed + !submarineContainer.Children.Any() || // We lack a GUI displayedSubs.Any(s => s != item.Submarine && !item.Submarine.DockedTo.Contains(s))) //displaying a sub that shouldn't be displayed { CreateHUD(); @@ -116,21 +117,23 @@ namespace Barotrauma.Items.Components private void DrawHUDFront(SpriteBatch spriteBatch, GUICustomComponent container) { if (Voltage < MinVoltage) - { + { Vector2 textSize = GUI.Font.MeasureString(noPowerTip); Vector2 textPos = GuiFrame.Rect.Center.ToVector2(); GUI.DrawString(spriteBatch, textPos - textSize / 2, noPowerTip, - GUI.Style.Orange * (float)Math.Abs(Math.Sin(Timing.TotalTime)), Color.Black * 0.8f, font: GUI.SubHeadingFont); + GUI.Style.Orange * (float)Math.Abs(Math.Sin(Timing.TotalTime)), Color.Black * 0.8f, font: GUI.SubHeadingFont); return; } + if (!submarineContainer.Children.Any()) { return; } + foreach (GUIComponent child in submarineContainer.Children.First().Children) { if (child.UserData is Hull hull) { - if (hull.Submarine == null || !hull.Submarine.IsOutpost) { continue; } - string text = TextManager.GetWithVariable("MiniMapOutpostDockingInfo", "[outpost]", hull.Submarine.Name); + if (hull.Submarine == null || !hull.Submarine.Info.IsOutpost) { continue; } + string text = TextManager.GetWithVariable("MiniMapOutpostDockingInfo", "[outpost]", hull.Submarine.Info.Name); Vector2 textSize = GUI.Font.MeasureString(text); Vector2 textPos = child.Center; if (textPos.X + textSize.X / 2 > submarineContainer.Rect.Right) @@ -151,7 +154,7 @@ namespace Barotrauma.Items.Components foreach (Hull hull in Hull.hullList) { - var hullFrame = submarineContainer.Children.First().FindChild(hull); + var hullFrame = submarineContainer.Children.FirstOrDefault()?.FindChild(hull); if (hullFrame == null) { continue; } if (GUI.MouseOn == hullFrame || hullFrame.IsParentOf(GUI.MouseOn)) @@ -175,7 +178,7 @@ namespace Barotrauma.Items.Components foreach (Hull hull in Hull.hullList) { if (hull.Submarine == null) continue; - var hullFrame = submarineContainer.Children.First().FindChild(hull); + var hullFrame = submarineContainer.Children.FirstOrDefault()?.FindChild(hull); if (hullFrame == null) continue; hullDatas.TryGetValue(hull, out HullData hullData); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs index f0384d4b5..069231f64 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Pump.cs @@ -99,6 +99,10 @@ namespace Barotrauma.Items.Components Step = 0.05f, OnMoved = (GUIScrollBar scrollBar, float barScroll) => { + if (pumpSpeedLockTimer <= 0.0f) + { + targetLevel = null; + } float newValue = barScroll * 200.0f - 100.0f; if (Math.Abs(newValue - FlowPercentage) < 0.1f) { return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs index 1604387a7..2ddb5a88f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Reactor.cs @@ -192,9 +192,10 @@ namespace Barotrauma.Items.Components { RelativeOffset = new Vector2(0, fissionMeter.RectTransform.RelativeOffset.Y + meterSize.Y) }, - style: "DeviceSlider", barSize: 0.1f) + style: "DeviceSlider", barSize: 0.15f) { Enabled = false, + Step = 1.0f / 255, OnMoved = (GUIScrollBar bar, float scrollAmount) => { LastUser = Character.Controlled; @@ -209,9 +210,10 @@ namespace Barotrauma.Items.Components { RelativeOffset = new Vector2(0, turbineMeter.RectTransform.RelativeOffset.Y + meterSize.Y) }, - style: "DeviceSlider", barSize: 0.1f, isHorizontal: true) + style: "DeviceSlider", barSize: 0.15f, isHorizontal: true) { Enabled = false, + Step = 1.0f / 255, OnMoved = (GUIScrollBar bar, float scrollAmount) => { LastUser = Character.Controlled; @@ -715,8 +717,14 @@ namespace Barotrauma.Items.Components targetTurbineOutput = msg.ReadRangedSingle(0.0f, 100.0f, 8); degreeOfSuccess = msg.ReadRangedSingle(0.0f, 1.0f, 8); - FissionRateScrollBar.BarScroll = targetFissionRate / 100.0f; - TurbineOutputScrollBar.BarScroll = targetTurbineOutput / 100.0f; + if (Math.Abs(FissionRateScrollBar.BarScroll - targetFissionRate / 100.0f) > 0.01f) + { + FissionRateScrollBar.BarScroll = targetFissionRate / 100.0f; + } + if (Math.Abs(TurbineOutputScrollBar.BarScroll - targetTurbineOutput / 100.0f) > 0.01f) + { + TurbineOutputScrollBar.BarScroll = targetTurbineOutput / 100.0f; + } IsActive = true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index d1f5e4439..c1ad43075 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -490,6 +490,7 @@ namespace Barotrauma.Items.Components disruptedDirections.Clear(); foreach (AITarget t in AITarget.List) { + if (t.Entity is Character c && c.Params.HideInSonar) { continue; } if (t.SoundRange <= 0.0f || float.IsNaN(t.SoundRange) || float.IsInfinity(t.SoundRange)) { continue; } float distSqr = Vector2.DistanceSquared(t.WorldPosition, transducerCenter); @@ -647,6 +648,8 @@ namespace Barotrauma.Items.Components if (GameMain.GameSession == null) { return; } + if (Level.Loaded == null) { return; } + DrawMarker(spriteBatch, GameMain.GameSession.StartLocation.Name, "outpost", @@ -699,8 +702,8 @@ namespace Barotrauma.Items.Components if (sub.WorldPosition.Y > Level.Loaded.Size.Y) { continue; } DrawMarker(spriteBatch, - sub.Name, - sub.HasTag(SubmarineTag.Shuttle) ? "shuttle" : "submarine", + sub.Info.DisplayName, + sub.Info.HasTag(SubmarineTag.Shuttle) ? "shuttle" : "submarine", sub.WorldPosition - transducerCenter, displayScale, center, DisplayRadius * 0.95f); } @@ -799,8 +802,11 @@ namespace Barotrauma.Items.Components { if (Level.Loaded != null && dockingPort.Item.Submarine.WorldPosition.Y > Level.Loaded.Size.Y) { continue; } + if (dockingPort.Item.Submarine == null) { continue; } + if (dockingPort.Item.Submarine.Info.IsWreck) { continue; } + //don't show the docking ports of the opposing team on the sonar - if (item.Submarine != null && dockingPort.Item.Submarine != null) + if (item.Submarine != null) { if ((dockingPort.Item.Submarine.TeamID == Character.TeamType.Team1 && item.Submarine.TeamID == Character.TeamType.Team2) || (dockingPort.Item.Submarine.TeamID == Character.TeamType.Team2 && item.Submarine.TeamID == Character.TeamType.Team1)) @@ -1125,6 +1131,7 @@ namespace Barotrauma.Items.Components foreach (Character c in Character.CharacterList) { if (c.AnimController.CurrentHull != null || !c.Enabled) { continue; } + if (c.Params.HideInSonar) { continue; } if (DetectSubmarineWalls && c.AnimController.CurrentHull == null && item.CurrentHull != null) { continue; } if (c.AnimController.SimplePhysicsEnabled) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index 07bd0f3d9..e73a9ff5d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -46,6 +46,8 @@ namespace Barotrauma.Items.Components private float checkConnectedPortsTimer; private const float CheckConnectedPortsInterval = 1.0f; + public DockingPort ActiveDockingSource, DockingTarget; + private Vector2 keyboardInput = Vector2.Zero; private float inputCumulation; @@ -665,7 +667,7 @@ namespace Barotrauma.Items.Components if (Vector2.DistanceSquared(PlayerInput.MousePosition, steerArea.Rect.Center.ToVector2()) < steerRadius * steerRadius) { - if (PlayerInput.PrimaryMouseButtonHeld() && !CrewManager.IsCommandInterfaceOpen && !GameSession.IsInfoFrameOpen) + if (PlayerInput.PrimaryMouseButtonHeld() && !CrewManager.IsCommandInterfaceOpen && !GameSession.IsTabMenuOpen) { Vector2 displaySubPos = (-sonar.DisplayOffset * sonar.Zoom) / sonar.Range * sonar.DisplayRadius * sonar.Zoom; displaySubPos.Y = -displaySubPos.Y; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs new file mode 100644 index 000000000..765a44282 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs @@ -0,0 +1,76 @@ +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.Xml.Linq; + +namespace Barotrauma.Items.Components +{ + partial class Projectile : ItemComponent + { + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + { + bool isStuck = msg.ReadBoolean(); + if (isStuck) + { + ushort submarineID = msg.ReadUInt16(); + ushort hullID = msg.ReadUInt16(); + Vector2 simPosition = new Vector2( + msg.ReadSingle(), + msg.ReadSingle()); + Vector2 axis = new Vector2( + msg.ReadSingle(), + msg.ReadSingle()); + UInt16 entityID = msg.ReadUInt16(); + + Entity entity = Entity.FindEntityByID(entityID); + Submarine submarine = Entity.FindEntityByID(submarineID) as Submarine; + Hull hull = Entity.FindEntityByID(hullID) as Hull; + item.Submarine = submarine; + item.CurrentHull = hull; + item.body.SetTransform(simPosition, item.body.Rotation); + if (entity is Character character) + { + byte limbIndex = msg.ReadByte(); + if (limbIndex >= character.AnimController.Limbs.Length) + { + DebugConsole.ThrowError($"Failed to read a projectile update from the server. Limb index out of bounds ({limbIndex}, character: {character.ToString()})"); + return; + } + if (character.Removed) { return; } + var limb = character.AnimController.Limbs[limbIndex]; + StickToTarget(limb.body.FarseerBody, axis); + } + else if (entity is Structure structure) + { + byte bodyIndex = msg.ReadByte(); + if (bodyIndex == 255) { bodyIndex = 0; } + if (bodyIndex >= structure.Bodies.Count) + { + DebugConsole.ThrowError($"Failed to read a projectile update from the server. Structure body index out of bounds ({bodyIndex}, structure: {structure.ToString()})"); + return; + } + var body = structure.Bodies[bodyIndex]; + StickToTarget(body, axis); + } + else if (entity is Item item) + { + if (item.Removed) { return; } + StickToTarget(item.body.FarseerBody, axis); + } + else if (entity is Submarine sub) + { + StickToTarget(sub.PhysicsBody.FarseerBody, axis); + } + else + { + DebugConsole.ThrowError($"Failed to read a projectile update from the server. Invalid stick target ({entity?.ToString() ?? "null"}, {entityID})"); + } + } + else + { + Unstick(); + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs index 8caf52a49..ec7a53bd1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Repairable.cs @@ -117,9 +117,18 @@ namespace Barotrauma.Items.Components case "emitter": case "particleemitter": particleEmitters.Add(new ParticleEmitter(subElement)); - particleEmitterConditionRanges.Add(new Vector2( - subElement.GetAttributeFloat("mincondition", 0.0f), - subElement.GetAttributeFloat("maxcondition", 100.0f))); + float minCondition = subElement.GetAttributeFloat("mincondition", 0.0f); + float maxCondition = subElement.GetAttributeFloat("maxcondition", 100.0f); + + if (maxCondition < minCondition) + { + DebugConsole.ThrowError("Invalid damage particle configuration in the Repairable component of " + item.Name + ". MaxCondition needs to be larger than MinCondition."); + float temp = maxCondition; + maxCondition = minCondition; + minCondition = temp; + } + particleEmitterConditionRanges.Add(new Vector2(minCondition, maxCondition)); + break; } } @@ -236,15 +245,7 @@ namespace Barotrauma.Items.Components DeteriorateAlways = msg.ReadBoolean(); ushort currentFixerID = msg.ReadUInt16(); currentFixerAction = (FixActions)msg.ReadRangedInteger(0, 2); - - if (currentFixerID == 0) - { - CurrentFixer = null; - } - else - { - CurrentFixer = Entity.FindEntityByID(currentFixerID) as Character; - } + CurrentFixer = currentFixerID != 0 ? Entity.FindEntityByID(currentFixerID) as Character : null; } public void ClientWrite(IWriteMessage msg, object[] extraData = null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs new file mode 100644 index 000000000..f72793dd5 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Rope.cs @@ -0,0 +1,172 @@ +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.Xml.Linq; + +namespace Barotrauma.Items.Components +{ + partial class Rope : ItemComponent, IDrawableComponent + { + private Sprite sprite, startSprite, endSprite; + + [Serialize(5, false)] + public int SpriteWidth + { + get; + set; + } + + [Serialize("255,255,255,255", false)] + public Color SpriteColor + { + get; + set; + } + + [Serialize(false, false)] + public bool Tile + { + get; + set; + } + + public Vector2 DrawSize + { + get + { + if (target == null || source == null) { return Vector2.Zero; } + return new Vector2( + Math.Abs(target.DrawPosition.X - source.DrawPosition.X), + Math.Abs(target.DrawPosition.Y - source.DrawPosition.Y)) * 1.5f; + } + } + + partial void InitProjSpecific(XElement element) + { + foreach (XElement subElement in element.Elements()) + { + switch (subElement.Name.ToString().ToLowerInvariant()) + { + case "sprite": + sprite = new Sprite(subElement); + break; + case "startsprite": + startSprite = new Sprite(subElement); + break; + case "endsprite": + endSprite = new Sprite(subElement); + break; + } + } + } + + public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1) + { + if (target == null) { return; } + + Vector2 startPos = new Vector2(source.DrawPosition.X, -source.DrawPosition.Y); + var turret = source?.GetComponent(); + if (turret != null) + { + startPos = new Vector2(source.WorldRect.X + turret.TransformedBarrelPos.X, -(source.WorldRect.Y - turret.TransformedBarrelPos.Y)); + if (turret.BarrelSprite != null) + { + startPos += new Vector2((float)Math.Cos(turret.Rotation), (float)Math.Sin(turret.Rotation)) * turret.BarrelSprite.size.Y * turret.BarrelSprite.RelativeOrigin.Y * item.Scale * 0.9f; + } + } + Vector2 endPos = new Vector2(target.DrawPosition.X, -target.DrawPosition.Y); + + if (Snapped) + { + float snapState = 1.0f - snapTimer / SnapAnimDuration; + Vector2 diff = target.DrawPosition - source.DrawPosition; + diff.Y = -diff.Y; + + int width = (int)(SpriteWidth * snapState); + if (width > 0.0f) + { + DrawRope(spriteBatch, endPos - diff * snapState * 0.5f, endPos, width); + DrawRope(spriteBatch, startPos, startPos + diff * snapState * 0.5f, width); + } + } + else + { + DrawRope(spriteBatch, startPos, endPos, SpriteWidth); + } + + if (startSprite != null || endSprite != null) + { + Vector2 dir = endPos - startPos; + float angle = (float)Math.Atan2(dir.Y, dir.X); + if (startSprite != null) + { + float depth = Math.Min(item.GetDrawDepth() + (startSprite.Depth - item.Sprite.Depth), 0.999f); + startSprite?.Draw(spriteBatch, startPos, SpriteColor, angle, depth: depth); + } + if (endSprite != null) + { + float depth = Math.Min(item.GetDrawDepth() + (endSprite.Depth - item.Sprite.Depth), 0.999f); + endSprite?.Draw(spriteBatch, endPos, SpriteColor, angle, depth: depth); + } + } + } + + private void DrawRope(SpriteBatch spriteBatch, Vector2 startPos, Vector2 endPos, int width) + { + float depth = sprite == null ? + item.Sprite.Depth + 0.001f : + Math.Min(item.GetDrawDepth() + (sprite.Depth - item.Sprite.Depth), 0.999f); + + if (sprite?.Texture == null) + { + GUI.DrawLine(spriteBatch, + startPos, + endPos, + SpriteColor, depth: depth, width: width); + return; + } + + if (Tile) + { + float length = Vector2.Distance(startPos, endPos); + Vector2 dir = (endPos - startPos) / length; + float x; + for (x = 0.0f; x <= length - sprite.size.X; x += sprite.size.X) + { + GUI.DrawLine(spriteBatch, sprite, + startPos + dir * (x - 5.0f), + startPos + dir * (x + sprite.size.X), + SpriteColor, depth: depth, width: width); + } + float leftOver = length - x; + if (leftOver > 0.0f) + { + GUI.DrawLine(spriteBatch, sprite, + startPos + dir * (x - 5.0f), + endPos, + SpriteColor, depth: depth, width: width); + } + } + else + { + GUI.DrawLine(spriteBatch, sprite, + startPos, + endPos, + SpriteColor, depth: depth, width: width); + } + } + + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) + { + snapped = msg.ReadBoolean(); + } + + protected override void RemoveComponentSpecific() + { + sprite?.Remove(); sprite = null; + startSprite?.Remove(); startSprite = null; + endSprite?.Remove(); endSprite = null; + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs index 214ac75b3..be7b5a42f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/CustomInterface.cs @@ -45,13 +45,48 @@ namespace Barotrauma.Items.Components float elementSize = Math.Min(1.0f / visibleElements.Count(), 1); foreach (CustomInterfaceElement ciElement in visibleElements) { - if (ciElement.ContinuousSignal) + if (!string.IsNullOrEmpty(ciElement.PropertyName)) + { + var layoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, elementSize), uiElementContainer.RectTransform), isHorizontal: true) + { + RelativeSpacing = 0.02f, + UserData = ciElement + }; + new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), layoutGroup.RectTransform), + TextManager.Get(ciElement.Label, returnNull: true) ?? ciElement.Label); + var textBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), layoutGroup.RectTransform), "", style: "GUITextBoxNoIcon") + { + OverflowClip = true, + UserData = ciElement + }; + //reset size restrictions set by the Style to make sure the elements can fit the interface + textBox.RectTransform.MinSize = textBox.Frame.RectTransform.MinSize = new Point(0, 0); + textBox.RectTransform.MaxSize = textBox.Frame.RectTransform.MaxSize = new Point(int.MaxValue, int.MaxValue); + textBox.OnDeselected += (tb, key) => + { + if (GameMain.Client == null) + { + TextChanged(tb.UserData as CustomInterfaceElement, textBox.Text); + } + else + { + item.CreateClientEvent(this); + } + }; + + textBox.OnEnterPressed += (tb, text) => + { + tb.Deselect(); + return true; + }; + uiElements.Add(textBox); + } + else if (ciElement.ContinuousSignal) { var tickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, elementSize), uiElementContainer.RectTransform) { MaxSize = ElementMaxSize - }, - TextManager.Get(ciElement.Label, returnNull: true) ?? ciElement.Label) + }, TextManager.Get(ciElement.Label, returnNull: true) ?? ciElement.Label) { UserData = ciElement }; @@ -148,7 +183,7 @@ namespace Barotrauma.Items.Components foreach (var uiElement in uiElements) { if (!(uiElement.UserData is CustomInterfaceElement element)) { continue; } - bool visible = Screen.Selected == GameMain.SubEditorScreen || element.StatusEffects.Any() || (element.Connection != null && element.Connection.Wires.Any(w => w != null)); + bool visible = Screen.Selected == GameMain.SubEditorScreen || element.StatusEffects.Any() || !string.IsNullOrEmpty(element.PropertyName) || (element.Connection != null && element.Connection.Wires.Any(w => w != null)); if (visible) { visibleElementCount++; } if (uiElement.Visible != visible) { @@ -188,6 +223,22 @@ namespace Barotrauma.Items.Components customInterfaceElementList[i].Label; tickBox.TextBlock.Wrap = tickBox.Text.Contains(' '); } + if (uiElements[i] is GUITextBox textBox) + { + var textBlock = textBox.Parent.GetChild(); + textBlock.Text = string.IsNullOrWhiteSpace(customInterfaceElementList[i].Label) ? + TextManager.GetWithVariable("connection.signaloutx", "[num]", (i + 1).ToString()) : + customInterfaceElementList[i].Label; + textBlock.Wrap = textBlock.Text.Contains(' '); + + foreach (ISerializableEntity e in item.AllPropertyObjects) + { + if (e.SerializableProperties.ContainsKey(customInterfaceElementList[i].PropertyName)) + { + textBox.Text = e.SerializableProperties[customInterfaceElementList[i].PropertyName].GetValue(e) as string; + } + } + } } uiElementContainer.Recalculate(); @@ -206,6 +257,10 @@ namespace Barotrauma.Items.Components { textBlocks.Add(tickBox.TextBlock); } + else if (element is GUILayoutGroup) + { + textBlocks.Add(element.GetChild()); + } } uiElementContainer.Recalculate(); GUITextBlock.AutoScaleAndNormalize(textBlocks); @@ -216,7 +271,11 @@ namespace Barotrauma.Items.Components //extradata contains an array of buttons clicked by the player (or nothing if the player didn't click anything) for (int i = 0; i < customInterfaceElementList.Count; i++) { - if (customInterfaceElementList[i].ContinuousSignal) + if (!string.IsNullOrEmpty(customInterfaceElementList[i].PropertyName)) + { + msg.Write(((GUITextBox)uiElements[i]).Text); + } + else if (customInterfaceElementList[i].ContinuousSignal) { msg.Write(((GUITickBox)uiElements[i]).Selected); } @@ -231,15 +290,22 @@ namespace Barotrauma.Items.Components { for (int i = 0; i < customInterfaceElementList.Count; i++) { - bool elementState = msg.ReadBoolean(); - if (customInterfaceElementList[i].ContinuousSignal) + if (!string.IsNullOrEmpty(customInterfaceElementList[i].PropertyName)) { - ((GUITickBox)uiElements[i]).Selected = elementState; - TickBoxToggled(customInterfaceElementList[i], elementState); + TextChanged(customInterfaceElementList[i], msg.ReadString()); } - else if (elementState) + else { - ButtonClicked(customInterfaceElementList[i]); + bool elementState = msg.ReadBoolean(); + if (customInterfaceElementList[i].ContinuousSignal) + { + ((GUITickBox)uiElements[i]).Selected = elementState; + TickBoxToggled(customInterfaceElementList[i], elementState); + } + else if (elementState) + { + ButtonClicked(customInterfaceElementList[i]); + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs index 16e806bb2..0cfef7f33 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Terminal.cs @@ -56,15 +56,6 @@ namespace Barotrauma.Items.Components }; } - public override void OnItemLoaded() - { - base.OnItemLoaded(); - if (!string.IsNullOrEmpty(DisplayedWelcomeMessage)) - { - ShowOnDisplay(DisplayedWelcomeMessage); - } - } - private void SendOutput(string input) { if (input.Length > MaxMessageLength) @@ -123,6 +114,11 @@ namespace Barotrauma.Items.Components public override void AddToGUIUpdateList() { base.AddToGUIUpdateList(); + if (!string.IsNullOrEmpty(DisplayedWelcomeMessage)) + { + ShowOnDisplay(DisplayedWelcomeMessage); + DisplayedWelcomeMessage = ""; + } if (shouldSelectInputBox) { inputBox.Select(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs index 7deb2705e..8f3cd4da6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Signal/Wire.cs @@ -17,14 +17,60 @@ namespace Barotrauma.Items.Components partial class WireSection { + public VertexPositionColorTexture[] vertices; + public VertexPositionColorTexture[] shiftedVertices; + + private float cachedWidth = 0f; + + private void RecalculateVertices(Wire wire, float width) + { + if (MathUtils.NearlyEqual(cachedWidth, width)) { return; } + cachedWidth = width; + + vertices = new VertexPositionColorTexture[4]; + + Vector2 expandDir = start-end; + expandDir.Normalize(); + float temp = expandDir.X; + expandDir.X = -expandDir.Y; + expandDir.Y = -temp; + + Rectangle srcRect = wire.wireSprite.SourceRect; + + expandDir *= width * srcRect.Height * 0.5f; + + Vector2 rectLocation = srcRect.Location.ToVector2(); + Vector2 rectSize = srcRect.Size.ToVector2(); + Vector2 textureSize = new Vector2(wire.wireSprite.Texture.Width, wire.wireSprite.Texture.Height); + + Vector2 topLeftUv = rectLocation / textureSize; + Vector2 bottomRightUv = (rectLocation + rectSize) / textureSize; + + Vector2 invStart = new Vector2(start.X, -start.Y); + Vector2 invEnd = new Vector2(end.X, -end.Y); + + vertices[0] = new VertexPositionColorTexture(new Vector3(invStart + expandDir, 0f), Color.White, topLeftUv); + vertices[2] = new VertexPositionColorTexture(new Vector3(invEnd + expandDir, 0f), Color.White, new Vector2(bottomRightUv.X, topLeftUv.Y)); + vertices[1] = new VertexPositionColorTexture(new Vector3(invStart - expandDir, 0f), Color.White, new Vector2(topLeftUv.X, bottomRightUv.Y)); + vertices[3] = new VertexPositionColorTexture(new Vector3(invEnd - expandDir, 0f), Color.White, bottomRightUv); + + shiftedVertices = (VertexPositionColorTexture[])vertices.Clone(); + } + public void Draw(SpriteBatch spriteBatch, Wire wire, Color color, Vector2 offset, float depth, float width = 0.3f) { + if (width <= 0f) { return; } + RecalculateVertices(wire, width); + + for (int i=0;i 0) @@ -167,13 +220,13 @@ namespace Barotrauma.Items.Components spriteBatch, this, new Vector2(nodes[nodes.Count - 1].X, nodes[nodes.Count - 1].Y) + drawOffset, new Vector2(newNodePos.X, newNodePos.Y) + drawOffset, - item.Color, 0.0f, 0.3f); + item.Color, 0.0f, Width); WireSection.Draw( spriteBatch, this, new Vector2(newNodePos.X, newNodePos.Y) + drawOffset, item.DrawPosition, - item.Color, itemDepth, 0.3f); + item.Color, itemDepth, Width); GUI.DrawRectangle(spriteBatch, new Vector2(newNodePos.X + drawOffset.X, -(newNodePos.Y + drawOffset.Y)) - Vector2.One * 3, Vector2.One * 6, item.Color); } @@ -183,7 +236,7 @@ namespace Barotrauma.Items.Components spriteBatch, this, new Vector2(nodes[nodes.Count - 1].X, nodes[nodes.Count - 1].Y) + drawOffset, item.DrawPosition, - item.Color, 0.0f, 0.3f); + item.Color, 0.0f, Width); } } } @@ -235,7 +288,7 @@ namespace Barotrauma.Items.Components Wire equippedWire = Character.Controlled?.SelectedItems[0]?.GetComponent() ?? Character.Controlled?.SelectedItems[1]?.GetComponent(); - if (equippedWire != null) + if (equippedWire != null && GUI.MouseOn == null) { if (PlayerInput.PrimaryMouseButtonClicked() && Character.Controlled.SelectedConstruction == null) { @@ -404,6 +457,20 @@ namespace Barotrauma.Items.Components } } + public bool IsMouseOn() + { + if (GUI.MouseOn == null) + { + Vector2 mousePos = GameMain.SubEditorScreen.Cam.ScreenToWorld(PlayerInput.MousePosition); + if (item.Submarine != null) { mousePos -= (item.Submarine.Position + item.Submarine.HiddenSubPosition); } + + if (GetClosestNodeIndex(mousePos, 10, out _) > -1) { return true; } + if (GetClosestSectionIndex(mousePos, 10, out _) > -1) { return true; } + } + + return false; + } + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { int eventIndex = msg.ReadRangedInteger(0, (int)Math.Ceiling(MaxNodeCount / (float)MaxNodesPerNetworkEvent)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index ba394badc..c4f22334c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -19,6 +19,8 @@ namespace Barotrauma.Items.Components private float recoilTimer; + private float RetractionTime => Math.Max(Reload * RetractionDurationMultiplier, RecoilTime); + private RoundSound startMoveSound, endMoveSound, moveSound; private SoundChannel moveSoundChannel; @@ -83,6 +85,11 @@ namespace Barotrauma.Items.Components } } + public Sprite BarrelSprite + { + get { return barrelSprite; } + } + partial void InitProjSpecific(XElement element) { foreach (XElement subElement in element.Elements()) @@ -126,7 +133,7 @@ namespace Barotrauma.Items.Components partial void LaunchProjSpecific() { - recoilTimer = Math.Max(Reload, 0.1f); + recoilTimer = RetractionTime; PlaySound(ActionType.OnUse); Vector2 particlePos = new Vector2(item.WorldRect.X + transformedBarrelPos.X, item.WorldRect.Y - transformedBarrelPos.Y); foreach (ParticleEmitter emitter in particleEmitters) @@ -135,6 +142,12 @@ namespace Barotrauma.Items.Components } } + public override void UpdateBroken(float deltaTime, Camera cam) + { + base.UpdateBroken(deltaTime, cam); + recoilTimer -= deltaTime; + } + partial void UpdateProjSpecific(float deltaTime) { recoilTimer -= deltaTime; @@ -223,21 +236,30 @@ namespace Barotrauma.Items.Components public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1) { Vector2 drawPos = new Vector2(item.Rect.X + transformedBarrelPos.X, item.Rect.Y - transformedBarrelPos.Y); - if (item.Submarine != null) drawPos += item.Submarine.DrawPosition; + if (item.Submarine != null) + { + drawPos += item.Submarine.DrawPosition; + } drawPos.Y = -drawPos.Y; float recoilOffset = 0.0f; - if (RecoilDistance > 0.0f && recoilTimer > 0.0f) + if (Math.Abs(RecoilDistance) > 0.0f && recoilTimer > 0.0f) { - //move the barrel backwards 0.1 seconds after launching - if (recoilTimer >= Math.Max(Reload, 0.1f) - 0.1f) + float diff = RetractionTime - RecoilTime; + if (recoilTimer >= diff) { - recoilOffset = RecoilDistance * (1.0f - (recoilTimer - (Math.Max(Reload, 0.1f) - 0.1f)) / 0.1f); + //move the barrel backwards 0.1 seconds (defined by RecoilTime) after launching + recoilOffset = RecoilDistance * (1.0f - (recoilTimer - diff) / RecoilTime); + } + else if (recoilTimer <= diff - RetractionDelay) + { + //move back to normal position while reloading + float t = diff - RetractionDelay; + recoilOffset = RecoilDistance * recoilTimer / t; } - //move back to normal position while reloading else { - recoilOffset = RecoilDistance * recoilTimer / (Math.Max(Reload, 0.1f) - 0.1f); + recoilOffset = RecoilDistance; } } @@ -369,13 +391,26 @@ namespace Barotrauma.Items.Components tooltipOffset = new Vector2(size / 2 + 5, -10), inputAreaMargin = 20, RequireMouseOn = false - }; + }; widgets.Add(id, widget); initMethod?.Invoke(widget); } return widget; } + private void GetAvailablePower(out float availableCharge, out float availableCapacity) + { + var batteries = item.GetConnectedComponents(); + + availableCharge = 0.0f; + availableCapacity = 0.0f; + foreach (PowerContainer battery in batteries) + { + availableCharge += battery.Charge; + availableCapacity += battery.Capacity; + } + } + /// /// Returns correct angle between -2PI and +2PI /// @@ -488,17 +523,31 @@ namespace Barotrauma.Items.Components public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { UInt16 projectileID = msg.ReadUInt16(); - //projectile removed, do nothing - if (projectileID == 0) return; + float newTargetRotation = msg.ReadRangedSingle(minRotation, maxRotation, 16); - Item projectile = Entity.FindEntityByID(projectileID) as Item; - if (projectile == null) + if (Character.Controlled == null || user != Character.Controlled) { - DebugConsole.ThrowError("Failed to launch a projectile - item with the ID \"" + projectileID + " not found"); - return; + targetRotation = newTargetRotation; + } + + //projectile removed, do nothing + if (projectileID == 0) { return; } + + //ID ushort.MaxValue = launched without a projectile + if (projectileID == ushort.MaxValue) + { + Launch(null); + } + else + { + if (!(Entity.FindEntityByID(projectileID) is Item projectile)) + { + DebugConsole.ThrowError("Failed to launch a projectile - item with the ID \"" + projectileID + " not found"); + return; + } + Launch(projectile, launchRotation: newTargetRotation); } - Launch(projectile); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs index b789b4381..a49647775 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/DockingPort.cs @@ -150,8 +150,8 @@ namespace Barotrauma.Items.Components if (joint == null) { string errorMsg = "Error while reading a docking port network event (Dock method did not create a joint between the ports)." + - " Submarine: " + (item.Submarine?.Name ?? "null") + - ", target submarine: " + (DockingTarget.item.Submarine?.Name ?? "null"); + " Submarine: " + (item.Submarine?.Info.Name ?? "null") + + ", target submarine: " + (DockingTarget.item.Submarine?.Info.Name ?? "null"); if (item.Submarine?.ConnectedDockingPorts.ContainsKey(DockingTarget.item.Submarine) ?? false) { errorMsg += "\nAlready docked."; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs index acfde35ec..8b1b6ba49 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Inventory.cs @@ -45,12 +45,17 @@ namespace Barotrauma public float QuickUseTimer; public string QuickUseButtonToolTip; + public bool IsMoving = false; + private static Rectangle offScreenRect = new Rectangle(new Point(-1000, 0), Point.Zero); public GUIComponent.ComponentState EquipButtonState; public Rectangle EquipButtonRect { get { + // Returns a point off-screen, Rectangle.Empty places buttons in the top left of the screen + if (IsMoving) return offScreenRect; + int buttonDir = Math.Sign(SubInventoryDir); float sizeY = Inventory.UnequippedIndicator.size.Y * Inventory.UIScale * Inventory.IndicatorScaleAdjustment; @@ -186,7 +191,7 @@ namespace Barotrauma public Item Item; public bool IsSubSlot; public string Tooltip; - public List TooltipColorData; + public List TooltipRichTextData; public SlotReference(Inventory parentInventory, InventorySlot slot, int slotIndex, bool isSubSlot, Inventory subInventory = null) { @@ -196,7 +201,7 @@ namespace Barotrauma Inventory = subInventory; IsSubSlot = isSubSlot; Item = ParentInventory.Items[slotIndex]; - TooltipColorData = ColorData.GetColorData(GetTooltip(Item), out Tooltip); + TooltipRichTextData = RichTextData.GetRichTextData(GetTooltip(Item), out Tooltip); } private string GetTooltip(Item item) @@ -445,12 +450,33 @@ namespace Barotrauma } }*/ - bool mouseOn = interactRect.Contains(PlayerInput.MousePosition) && !Locked && !mouseOnGUI; + bool mouseOn = interactRect.Contains(PlayerInput.MousePosition) && !Locked && !mouseOnGUI && !slot.Disabled; + + // Delete item from container in sub editor + if (SubEditorScreen.IsSubEditor() && PlayerInput.IsCtrlDown()) + { + draggingItem = null; + var mouseDrag = SubEditorScreen.MouseDragStart != Vector2.Zero && Vector2.Distance(PlayerInput.MousePosition, SubEditorScreen.MouseDragStart) >= GUI.Scale * 20; + if (mouseOn && (PlayerInput.PrimaryMouseButtonClicked() || mouseDrag)) + { + if (item != null) + { + if (mouseDrag) { item.OwnInventory?.DeleteAllItems(); } + slot.ShowBorderHighlight(GUI.Style.Red, 0.1f, 0.4f); + if (!mouseDrag) + { + GUI.PlayUISound(GUISoundType.PickItem); + } + item.Remove(); + } + } + } + if (PlayerInput.LeftButtonHeld() && PlayerInput.RightButtonHeld()) { mouseOn = false; } - + if (selectedSlot != null && selectedSlot.Slot != slot) { //subinventory slot highlighted -> don't allow highlighting this one @@ -471,11 +497,14 @@ namespace Barotrauma // && //(highlightedSubInventories.Count == 0 || highlightedSubInventories.Contains(this) || highlightedSubInventorySlot?.Slot == slot || highlightedSubInventory.Owner == item)) { + slot.State = GUIComponent.ComponentState.Hover; if (selectedSlot == null || (!selectedSlot.IsSubSlot && isSubSlot)) { - selectedSlot = new SlotReference(this, slot, slotIndex, isSubSlot, Items[slotIndex]?.GetComponent()?.Inventory); + var slotRef = new SlotReference(this, slot, slotIndex, isSubSlot, Items[slotIndex]?.GetComponent()?.Inventory); + if (Screen.Selected is SubEditorScreen editor && !editor.WiringMode && slotRef.ParentInventory is CharacterInventory) { return; } + selectedSlot = slotRef; } if (draggingItem == null) @@ -663,6 +692,18 @@ namespace Barotrauma DrawSlot(spriteBatch, this, slots[i], Items[i], i, drawItem); } } + + /// + /// Check if the mouse is hovering on top of the slot + /// + /// The desired slot we want to check + /// True if our mouse is hover on the slot, false otherwise + public static bool IsMouseOnSlot(InventorySlot slot) + { + var rect = new Rectangle(slot.InteractRect.X, slot.InteractRect.Y, slot.InteractRect.Width, slot.InteractRect.Height); + rect.Offset(slot.DrawOffset); + return rect.Contains(PlayerInput.MousePosition); + } /// /// Is the mouse on any inventory element (slot, equip button, subinventory...) @@ -670,11 +711,12 @@ namespace Barotrauma /// public static bool IsMouseOnInventory() { + var isSubEditor = Screen.Selected is SubEditorScreen editor && !editor.WiringMode; if (Character.Controlled == null) return false; if (draggingItem != null || DraggingInventory != null) return true; - if (Character.Controlled.Inventory != null) + if (Character.Controlled.Inventory != null && !isSubEditor) { var inv = Character.Controlled.Inventory; for (var i = 0; i < inv.slots.Length; i++) @@ -694,7 +736,8 @@ namespace Barotrauma } } } - if (Character.Controlled.SelectedCharacter?.Inventory != null) + + if (Character.Controlled.SelectedCharacter?.Inventory != null && !isSubEditor) { var inv = Character.Controlled.SelectedCharacter.Inventory; for (var i = 0; i < inv.slots.Length; i++) @@ -825,9 +868,9 @@ namespace Barotrauma return CursorState.Default; } - protected static void DrawToolTip(SpriteBatch spriteBatch, string toolTip, Rectangle highlightedSlot, List colorData = null) + protected static void DrawToolTip(SpriteBatch spriteBatch, string toolTip, Rectangle highlightedSlot, List richTextData = null) { - GUIComponent.DrawToolTip(spriteBatch, toolTip, highlightedSlot, colorData); + GUIComponent.DrawToolTip(spriteBatch, toolTip, highlightedSlot, richTextData); } public void DrawSubInventory(SpriteBatch spriteBatch, int slotIndex) @@ -941,8 +984,33 @@ namespace Barotrauma } else { - GUI.PlayUISound(GUISoundType.DropItem); - draggingItem.Drop(Character.Controlled); + bool removed = false; + if (Screen.Selected is SubEditorScreen editor) + { + if (editor.EntityMenu.Rect.Contains(PlayerInput.MousePosition)) + { + draggingItem.Remove(); + removed = true; + } + else + { + if (editor.WiringMode) + { + draggingItem.Remove(); + removed = true; + } + else + { + draggingItem.Drop(Character.Controlled); + } + } + } + else + { + draggingItem.Drop(Character.Controlled); + } + + GUI.PlayUISound(removed ? GUISoundType.PickItem : GUISoundType.DropItem); } } else if (selectedSlot.ParentInventory.Items[selectedSlot.SlotIndex] != draggingItem) @@ -1033,7 +1101,9 @@ namespace Barotrauma } if (subSlot.Slot.SubInventoryDir < 0) { - hoverArea.Height -= hoverArea.Bottom - subSlot.Slot.Rect.Bottom; + // 24/2/2020 - the below statement makes the sub inventory extend all the way to the bottom of the screen because of a double negative + // Not sure if it's intentional or not but it was causing hover issues and disabling it seems to have no detrimental effects. + // hoverArea.Height -= hoverArea.Bottom - subSlot.Slot.Rect.Bottom; } else { @@ -1083,7 +1153,7 @@ namespace Barotrauma string toolTip = mouseOnHealthInterface ? TextManager.Get("QuickUseAction.UseTreatment") : Character.Controlled.FocusedItem != null ? TextManager.GetWithVariable("PutItemIn", "[itemname]", Character.Controlled.FocusedItem.Name, true) : - TextManager.Get("DropItem"); + TextManager.Get(Screen.Selected is SubEditorScreen editor && editor.EntityMenu.Rect.Contains(PlayerInput.MousePosition) ? "Delete" : "DropItem"); int textWidth = (int)Math.Max(GUI.Font.MeasureString(draggingItem.Name).X, GUI.SmallFont.MeasureString(toolTip).X); int textSpacing = (int)(15 * GUI.Scale); Point shadowBorders = (new Point(40, 10)).Multiply(GUI.Scale); @@ -1106,7 +1176,7 @@ namespace Barotrauma { Rectangle slotRect = selectedSlot.Slot.Rect; slotRect.Location += selectedSlot.Slot.DrawOffset.ToPoint(); - DrawToolTip(spriteBatch, selectedSlot.Tooltip, slotRect, selectedSlot.TooltipColorData); + DrawToolTip(spriteBatch, selectedSlot.Tooltip, slotRect, selectedSlot.TooltipRichTextData); } } @@ -1140,15 +1210,20 @@ namespace Barotrauma /*if (inventory != null && (CharacterInventory.PersonalSlots.HasFlag(type) || (inventory.isSubInventory && (inventory.Owner as Item) != null && (inventory.Owner as Item).AllowedSlots.Any(a => CharacterInventory.PersonalSlots.HasFlag(a))))) { - slotColor = slot.IsHighlighted ? GUIColorSettings.EquipmentSlotColor : GUIColorSettings.EquipmentSlotColor * 0.8f; + slotColor = slot.IsHighlighted ? GUI.Style.EquipmentSlotColor : GUI.Style.EquipmentSlotColor * 0.8f; } else { - slotColor = slot.IsHighlighted ? GUIColorSettings.InventorySlotColor : GUIColorSettings.InventorySlotColor * 0.8f; + slotColor = slot.IsHighlighted ? GUI.Style.InventorySlotColor : GUI.Style.InventorySlotColor * 0.8f; }*/ if (inventory != null && inventory.Locked) { slotColor = Color.Gray * 0.5f; } spriteBatch.Draw(slotSprite.Texture, rect, slotSprite.SourceRect, slotColor); + + if (SubEditorScreen.IsSubEditor() && PlayerInput.IsCtrlDown() && selectedSlot?.Slot == slot) + { + GUI.DrawRectangle(spriteBatch, rect, GUI.Style.Red * 0.3f, isFilled: true); + } bool canBePut = false; @@ -1203,13 +1278,15 @@ namespace Barotrauma dir < 0 ? rect.Bottom + HUDLayoutSettings.Padding / 2 : rect.Y - HUDLayoutSettings.Padding / 2 - ContainedIndicatorHeight, rect.Width, ContainedIndicatorHeight); containedIndicatorArea.Inflate(-4, 0); + Color backgroundColor = GUI.Style.ColorInventoryBackground; + if (itemContainer.ContainedStateIndicator?.Texture == null) { containedIndicatorArea.Inflate(0, -2); - GUI.DrawRectangle(spriteBatch, containedIndicatorArea, Color.Gray * 0.9f, true); + GUI.DrawRectangle(spriteBatch, containedIndicatorArea, backgroundColor, true); GUI.DrawRectangle(spriteBatch, new Rectangle(containedIndicatorArea.X, containedIndicatorArea.Y, (int)(containedIndicatorArea.Width * containedState), containedIndicatorArea.Height), - ToolBox.GradientLerp(containedState, Color.Red, Color.Orange, Color.LightGreen) * 0.8f, true); + ToolBox.GradientLerp(containedState, GUI.Style.ColorInventoryEmpty, GUI.Style.ColorInventoryHalf, GUI.Style.ColorInventoryFull) * 0.8f, true); GUI.DrawLine(spriteBatch, new Vector2(containedIndicatorArea.X + (int)(containedIndicatorArea.Width * containedState), containedIndicatorArea.Y), new Vector2(containedIndicatorArea.X + (int)(containedIndicatorArea.Width * containedState), containedIndicatorArea.Bottom), @@ -1228,12 +1305,12 @@ namespace Barotrauma } indicatorSprite.Draw(spriteBatch, containedIndicatorArea.Center.ToVector2(), - (inventory != null && inventory.Locked) ? Color.Gray * 0.5f : Color.Gray * 0.9f, + (inventory != null && inventory.Locked) ? backgroundColor * 0.5f : backgroundColor, origin: indicatorSprite.size / 2, rotate: 0.0f, scale: indicatorScale); - Color indicatorColor = ToolBox.GradientLerp(containedState, Color.Red, Color.Orange, Color.LightGreen); + Color indicatorColor = ToolBox.GradientLerp(containedState, GUI.Style.ColorInventoryEmpty, GUI.Style.ColorInventoryHalf, GUI.Style.ColorInventoryFull); if (inventory != null && inventory.Locked) { indicatorColor *= 0.5f; } spriteBatch.Draw(indicatorSprite.Texture, containedIndicatorArea.Center.ToVector2(), diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 755769bd2..7589211bc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -29,14 +29,8 @@ namespace Barotrauma private bool editingHUDRefreshPending; private float editingHUDRefreshTimer; - class SpriteState - { - public float RotationState; - public float OffsetState; - public bool IsActive = true; - } - private Dictionary spriteAnimState = new Dictionary(); + private readonly Dictionary spriteAnimState = new Dictionary(); private Sprite activeSprite; public override Sprite Sprite @@ -44,11 +38,20 @@ namespace Barotrauma get { return activeSprite; } } - public override bool DrawOverWater + public override Rectangle Rect { - get { return base.DrawOverWater || (GetComponent() != null && IsSelected); } + get { return base.Rect; } + set + { + cachedVisibleSize = null; + base.Rect = value; + } } + public override bool DrawBelowWater => (!(Screen.Selected is SubEditorScreen editor) || !editor.WiringMode || !isWire) && base.DrawBelowWater; + + public override bool DrawOverWater => base.DrawOverWater || (IsSelected || Screen.Selected is SubEditorScreen editor && editor.WiringMode) && isWire; + private GUITextBlock itemInUseWarning; private GUITextBlock ItemInUseWarning { @@ -160,8 +163,16 @@ namespace Barotrauma foreach (var decorativeSprite in ((ItemPrefab)prefab).DecorativeSprites) { decorativeSprite.Sprite.EnsureLazyLoaded(); - spriteAnimState.Add(decorativeSprite, new SpriteState()); + spriteAnimState.Add(decorativeSprite, new DecorativeSprite.State()); } + UpdateSpriteStates(0.0f); + } + + private Vector2? cachedVisibleSize; + + public void ResetCachedVisibleSize() + { + cachedVisibleSize = null; } public override bool IsVisible(Rectangle worldView) @@ -173,19 +184,28 @@ namespace Barotrauma } //no drawable components and the body has been disabled = nothing to draw - if (drawableComponents.Count == 0 && body != null && !body.Enabled) + if (!hasComponentsToDraw && body != null && !body.Enabled) { return false; } - float padding = 100.0f; - Vector2 size = new Vector2(rect.Width + padding, rect.Height + padding); - foreach (IDrawableComponent drawable in drawableComponents) + Vector2 size; + if (cachedVisibleSize.HasValue) { - size.X = Math.Max(drawable.DrawSize.X, size.X); - size.Y = Math.Max(drawable.DrawSize.Y, size.Y); + size = cachedVisibleSize.Value; + } + else + { + float padding = 100.0f; + size = new Vector2(rect.Width + padding, rect.Height + padding); + foreach (IDrawableComponent drawable in drawableComponents) + { + size.X = Math.Max(drawable.DrawSize.X, size.X); + size.Y = Math.Max(drawable.DrawSize.Y, size.Y); + } + size *= 0.5f; + cachedVisibleSize = size; } - size *= 0.5f; //cache world position so we don't need to calculate it 4 times Vector2 worldPosition = WorldPosition; @@ -197,8 +217,8 @@ namespace Barotrauma public override void Draw(SpriteBatch spriteBatch, bool editing, bool back = true) { - if (!Visible || (!editing && HiddenInGame)) return; - if (editing && !ShowItems) return; + if (!Visible || (!editing && HiddenInGame)) { return; } + if (editing && !ShowItems) { return; } Color color = IsHighlighted && !GUI.DisableItemHighlights && Screen.Selected != GameMain.GameScreen ? GUI.Style.Orange : GetSpriteColor(); //if (IsSelected && editing) color = Color.Lerp(color, Color.Gold, 0.5f); @@ -268,7 +288,7 @@ namespace Barotrauma float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState); Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState) * Scale; decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color, - SpriteRotation + rotation, Scale, activeSprite.effects, + SpriteRotation + rotation, decorativeSprite.Scale * Scale, activeSprite.effects, depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth), 0.999f)); } } @@ -312,7 +332,7 @@ namespace Barotrauma Vector2 transformedOffset = new Vector2(ca * offset.X + sa * offset.Y, -sa * offset.X + ca * offset.Y); decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + transformedOffset.X, -(DrawPosition.Y + transformedOffset.Y)), color, - -body.Rotation + rotation, Scale, activeSprite.effects, + -body.Rotation + rotation, decorativeSprite.Scale * Scale, activeSprite.effects, depth: depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth)); } } @@ -398,46 +418,7 @@ namespace Barotrauma public void UpdateSpriteStates(float deltaTime) { - foreach (int spriteGroup in Prefab.DecorativeSpriteGroups.Keys) - { - for (int i = 0; i < Prefab.DecorativeSpriteGroups[spriteGroup].Count; i++) - { - var decorativeSprite = Prefab.DecorativeSpriteGroups[spriteGroup][i]; - if (decorativeSprite == null) { continue; } - if (spriteGroup > 0) - { - int activeSpriteIndex = ID % Prefab.DecorativeSpriteGroups[spriteGroup].Count; - if (i != activeSpriteIndex) - { - spriteAnimState[decorativeSprite].IsActive = false; - continue; - } - } - - //check if the sprite is active (whether it should be drawn or not) - var spriteState = spriteAnimState[decorativeSprite]; - spriteState.IsActive = true; - foreach (PropertyConditional conditional in decorativeSprite.IsActiveConditionals) - { - if (!ConditionalMatches(conditional)) - { - spriteState.IsActive = false; - break; - } - } - if (!spriteState.IsActive) { continue; } - - //check if the sprite should be animated - bool animate = true; - foreach (PropertyConditional conditional in decorativeSprite.AnimationConditionals) - { - if (!ConditionalMatches(conditional)) { animate = false; break; } - } - if (!animate) { continue; } - spriteState.OffsetState += deltaTime; - spriteState.RotationState += deltaTime; - } - } + DecorativeSprite.UpdateSpriteStates(Prefab.DecorativeSpriteGroups, spriteAnimState, ID, deltaTime, ConditionalMatches); } public override void UpdateEditing(Camera cam) @@ -716,6 +697,13 @@ namespace Barotrauma HUDLayoutSettings.ChatBoxArea.Width + disallowedPadding, HUDLayoutSettings.ChatBoxArea.Height)); } + if (Screen.Selected is SubEditorScreen editor) + { + disallowedAreas.Add(editor.EntityMenu.Rect); + disallowedAreas.Add(editor.TopPanel.Rect); + disallowedAreas.Add(editor.ToggleEntityMenuButton.Rect); + } + GUI.PreventElementOverlap(elementsToMove, disallowedAreas, new Rectangle( 0, 20, @@ -871,19 +859,21 @@ namespace Barotrauma } readonly List texts = new List(); - public List GetHUDTexts(Character character) + public List GetHUDTexts(Character character, bool recreateHudTexts = true) { + // Always create the texts if they have not yet been created + if (texts.Any() && !recreateHudTexts) { return texts; } texts.Clear(); foreach (ItemComponent ic in components) { - if (string.IsNullOrEmpty(ic.DisplayMsg)) continue; - if (!ic.CanBePicked && !ic.CanBeSelected) continue; - if (ic is Holdable holdable && !holdable.CanBeDeattached()) continue; + if (string.IsNullOrEmpty(ic.DisplayMsg)) { continue; } + if (!ic.CanBePicked && !ic.CanBeSelected) { continue; } + if (ic is Holdable holdable && !holdable.CanBeDeattached()) { continue; } Color color = Color.Gray; if (ic.HasRequiredItems(character, false)) { - if (ic is Repairable repairable) + if (ic is Repairable) { if (!IsFullCondition) { color = Color.Cyan; } } @@ -892,9 +882,12 @@ namespace Barotrauma color = Color.Cyan; } } - texts.Add(new ColoredText(ic.DisplayMsg, color, false)); } + if ((PlayerInput.KeyDown(Keys.LeftShift) || PlayerInput.KeyDown(Keys.RightShift)) && CrewManager.DoesItemHaveContextualOrders(this)) + { + texts.Add(new ColoredText(TextManager.ParseInputTypes(TextManager.Get("itemmsgcontextualorders")), Color.Cyan, false)); + } return texts; } @@ -902,17 +895,17 @@ namespace Barotrauma { if (Screen.Selected is SubEditorScreen) { - if (editingHUD != null && editingHUD.UserData == this) editingHUD.AddToGUIUpdateList(); + if (editingHUD != null && editingHUD.UserData == this) { editingHUD.AddToGUIUpdateList(); } } else { if (HasInGameEditableProperties) { - if (editingHUD != null && editingHUD.UserData == this) editingHUD.AddToGUIUpdateList(); + if (editingHUD != null && editingHUD.UserData == this) { editingHUD.AddToGUIUpdateList(); } } } - if (Character.Controlled != null && Character.Controlled?.SelectedConstruction != this) return; + if (Character.Controlled != null && Character.Controlled?.SelectedConstruction != this) { return; } bool needsLayoutUpdate = false; foreach (ItemComponent ic in activeHUDs) @@ -1218,6 +1211,8 @@ namespace Barotrauma } } + byte bodyType = msg.ReadByte(); + byte teamID = msg.ReadByte(); bool tagsChanged = msg.ReadBoolean(); string tags = ""; @@ -1256,8 +1251,8 @@ namespace Barotrauma { if (itemContainerIndex < 0 || itemContainerIndex >= parentItem.components.Count) { - string errorMsg = "Failed to spawn item \"" + (itemIdentifier ?? "null") + - "\" in the inventory of \"" + parentItem.prefab.Identifier + "\" (component index out of range). Index: " + itemContainerIndex + ", components: " + parentItem.components.Count + "."; + string errorMsg = + $"Failed to spawn item \"{(itemIdentifier ?? "null")}\" in the inventory of \"{parentItem.prefab.Identifier} ({parentItem.ID})\" (component index out of range). Index: {itemContainerIndex}, components: {parentItem.components.Count}."; GameAnalyticsManager.AddErrorEventOnce("Item.ReadSpawnData:ContainerIndexOutOfRange" + (itemName ?? "null") + (itemIdentifier ?? "null"), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); @@ -1275,6 +1270,11 @@ namespace Barotrauma ID = itemId }; + if (item.body != null) + { + item.body.BodyType = (BodyType)bodyType; + } + foreach (WifiComponent wifiComponent in item.GetComponents()) { wifiComponent.TeamID = (Character.TeamType)teamID; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs index 04bd09373..1b11c5ead 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs @@ -66,16 +66,17 @@ namespace Barotrauma [Serialize("", false)] public string ImpactSoundTag { get; private set; } - public override void UpdatePlacing(Camera cam) { Vector2 position = Submarine.MouseToWorldGrid(cam, Submarine.MainSub); - + if (PlayerInput.SecondaryMouseButtonClicked()) { selected = null; return; } + + var potentialContainer = MapEntity.GetPotentialContainer(position); if (!ResizeHorizontal && !ResizeVertical) { @@ -88,6 +89,14 @@ namespace Barotrauma item.SetTransform(ConvertUnits.ToSimUnits(Submarine.MainSub == null ? item.Position : item.Position - Submarine.MainSub.Position), 0.0f); item.FindHull(); + if (PlayerInput.IsShiftDown()) + { + if (potentialContainer?.OwnInventory?.TryPutItem(item, Character.Controlled) ?? false) + { + GUI.PlayUISound(GUISoundType.PickItem); + } + } + placePosition = Vector2.Zero; return; } @@ -124,6 +133,12 @@ namespace Barotrauma } } + if (potentialContainer != null) + { + potentialContainer.IsHighlighted = true; + } + + //if (PlayerInput.GetMouseState.RightButton == ButtonState.Pressed) selected = null; } @@ -141,26 +156,10 @@ namespace Barotrauma if (!ResizeHorizontal && !ResizeVertical) { sprite.Draw(spriteBatch, new Vector2(position.X, -position.Y) + sprite.size / 2.0f * Scale, SpriteColor, scale: Scale); - } else { - Vector2 placeSize = size; - if (placePosition == Vector2.Zero) - { - if (PlayerInput.PrimaryMouseButtonHeld()) placePosition = position; - } - else - { - if (ResizeHorizontal) - placeSize.X = Math.Max(position.X - placePosition.X, size.X); - if (ResizeVertical) - placeSize.Y = Math.Max(placePosition.Y - position.Y, size.Y); - - position = placePosition; - } - - if (sprite != null) sprite.DrawTiled(spriteBatch, new Vector2(position.X, -position.Y), placeSize, color: SpriteColor); + sprite?.DrawTiled(spriteBatch, new Vector2(position.X, -position.Y), size, color: SpriteColor); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs index b2fc35ae7..ac8c01edf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Gap.cs @@ -24,7 +24,7 @@ namespace Barotrauma public override void Draw(SpriteBatch sb, bool editing, bool back = true) { - if (GameMain.DebugDraw) + if (!GameMain.DebugDraw && Screen.Selected.Cam.Zoom > 0.1f) { Vector2 center = new Vector2(WorldRect.X + rect.Width / 2.0f, -(WorldRect.Y - rect.Height / 2.0f)); GUI.DrawLine(sb, center, center + new Vector2(flowForce.X, -flowForce.Y) / 10.0f, GUI.Style.Red); @@ -41,7 +41,7 @@ namespace Barotrauma } } - if (!editing || !ShowGaps) return; + if (!editing || !ShowGaps) { return; } Color clr = (open == 0.0f) ? GUI.Style.Red : Color.Cyan; if (IsHighlighted) clr = Color.Gold; @@ -76,32 +76,35 @@ namespace Barotrauma clr * 0.6f, width: lineWidth); } - for (int i = 0; i < linkedTo.Count; i++) + if (linkedTo.Count != 2 || linkedTo[0] != linkedTo[1]) { - Vector2 dir = IsHorizontal ? - new Vector2(Math.Sign(linkedTo[i].Rect.Center.X - rect.Center.X), 0.0f) - : new Vector2(0.0f, Math.Sign((linkedTo[i].Rect.Y - linkedTo[i].Rect.Height / 2.0f) - (rect.Y - rect.Height / 2.0f))); - - Vector2 arrowPos = new Vector2(WorldRect.Center.X, -(WorldRect.Y - WorldRect.Height / 2)); - arrowPos += new Vector2(dir.X * (WorldRect.Width / 2), dir.Y * (WorldRect.Height / 2)); - - float arrowWidth = 32.0f; - float arrowSize = 15.0f; - - bool invalidDir = false; - if (dir == Vector2.Zero) + for (int i = 0; i < linkedTo.Count; i++) { - invalidDir = true; - dir = IsHorizontal ? Vector2.UnitX : Vector2.UnitY; - } + Vector2 dir = IsHorizontal ? + new Vector2(Math.Sign(linkedTo[i].Rect.Center.X - rect.Center.X), 0.0f) + : new Vector2(0.0f, Math.Sign((linkedTo[i].Rect.Y - linkedTo[i].Rect.Height / 2.0f) - (rect.Y - rect.Height / 2.0f))); - GUI.Arrow.Draw(sb, - arrowPos, invalidDir ? Color.Red : clr * 0.8f, - GUI.Arrow.Origin, MathUtils.VectorToAngle(dir) + MathHelper.PiOver2, - IsHorizontal ? - new Vector2(Math.Min(rect.Height, arrowWidth) / GUI.Arrow.size.X, arrowSize / GUI.Arrow.size.Y) : - new Vector2(Math.Min(rect.Width, arrowWidth) / GUI.Arrow.size.X, arrowSize / GUI.Arrow.size.Y), - SpriteEffects.None, depth); + Vector2 arrowPos = new Vector2(WorldRect.Center.X, -(WorldRect.Y - WorldRect.Height / 2)); + arrowPos += new Vector2(dir.X * (WorldRect.Width / 2), dir.Y * (WorldRect.Height / 2)); + + float arrowWidth = 32.0f; + float arrowSize = 15.0f; + + bool invalidDir = false; + if (dir == Vector2.Zero) + { + invalidDir = true; + dir = IsHorizontal ? Vector2.UnitX : Vector2.UnitY; + } + + GUI.Arrow.Draw(sb, + arrowPos, invalidDir ? Color.Red : clr * 0.8f, + GUI.Arrow.Origin, MathUtils.VectorToAngle(dir) + MathHelper.PiOver2, + IsHorizontal ? + new Vector2(Math.Min(rect.Height, arrowWidth) / GUI.Arrow.size.X, arrowSize / GUI.Arrow.size.Y) : + new Vector2(Math.Min(rect.Width, arrowWidth) / GUI.Arrow.size.X, arrowSize / GUI.Arrow.size.Y), + SpriteEffects.None, depth); + } } if (IsSelected) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs index f7084fce6..038062bc7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Hull.cs @@ -246,7 +246,7 @@ namespace Barotrauma if (!ShowHulls && !GameMain.DebugDraw) return; - if (!editing && !GameMain.DebugDraw) return; + if (!editing && (!GameMain.DebugDraw || Screen.Selected.Cam.Zoom < 0.1f)) return; Rectangle drawRect = Submarine == null ? rect : new Rectangle((int)(Submarine.DrawPosition.X + rect.X), (int)(Submarine.DrawPosition.Y + rect.Y), rect.Width, rect.Height); @@ -269,7 +269,8 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, new Rectangle(drawRect.Center.X, -drawRect.Y + drawRect.Height / 2, 10, (int)(100 * Math.Min(waterVolume / Volume, 1.0f))), Color.Cyan, true); if (WaterVolume > Volume) { - GUI.DrawRectangle(spriteBatch, new Rectangle(drawRect.Center.X, -drawRect.Y + drawRect.Height / 2, 10, (int)(100 * (waterVolume - Volume) / MaxCompress)), GUI.Style.Red, true); + float maxExcessWater = Volume * MaxCompress; + GUI.DrawRectangle(spriteBatch, new Rectangle(drawRect.Center.X, -drawRect.Y + drawRect.Height / 2, 10, (int)(100 * (waterVolume - Volume) / maxExcessWater)), GUI.Style.Red, true); } GUI.DrawRectangle(spriteBatch, new Rectangle(drawRect.Center.X, -drawRect.Y + drawRect.Height / 2, 10, 100), Color.Black); @@ -450,7 +451,7 @@ namespace Barotrauma } //we only create a new quad if this is the first or the last one, of if there's a wave large enough that we need more geometry - if (i == end - 1 || i == start || Math.Abs(prevCorners[1].Y - corners[3].Y) > 1.0f) + if (i == end - 1 || i == start || Math.Abs(prevCorners[1].Y - corners[2].Y) > 0.01f) { renderer.vertices[renderer.PositionInBuffer] = new VertexPositionTexture(prevCorners[0], prevUVs[0]); renderer.vertices[renderer.PositionInBuffer + 1] = new VertexPositionTexture(corners[1], uvCoords[1]); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs index 0fcff3a6d..2a7258acb 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/ItemAssemblyPrefab.cs @@ -33,7 +33,9 @@ namespace Barotrauma foreach (Pair entity in DisplayEntities) { Rectangle drawRect = entity.Second; - drawRect.Location += Submarine.MouseToWorldGrid(cam, Submarine.MainSub).ToPoint(); + + drawRect.Location += placePosition != Vector2.Zero ? placePosition.ToPoint() : Submarine.MouseToWorldGrid(cam, Submarine.MainSub).ToPoint(); + entity.First.DrawPlacing(spriteBatch, drawRect, entity.First.Scale); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreaturePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreaturePrefab.cs index 72d5db102..94501138a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreaturePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/BackgroundCreatures/BackgroundCreaturePrefab.cs @@ -39,7 +39,7 @@ namespace Barotrauma foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "sprite") continue; + if (!subElement.Name.ToString().Equals("sprite", System.StringComparison.OrdinalIgnoreCase)) { continue; } Sprite = new Sprite(subElement, lazyLoad: true); break; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs index 41ff02ce6..a59df6509 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/Level.cs @@ -1,7 +1,7 @@ using Barotrauma.Networking; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; -using FarseerPhysics.Dynamics; +using System; using System.Linq; using System.Collections.Generic; using FarseerPhysics; @@ -54,7 +54,7 @@ namespace Barotrauma if (renderer == null) return; renderer.Draw(spriteBatch, cam); - if (GameMain.DebugDraw) + if (GameMain.DebugDraw && Screen.Selected.Cam.Zoom > 0.1f) { foreach (InterestingPosition pos in positionsOfInterest) { @@ -78,6 +78,35 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, ruinArea, Color.DarkSlateBlue, false, 0, 5); } + + foreach (var positions in wreckPositions.Values) + { + for (int i = 0; i < positions.Count; i++) + { + float t = (i + 1) / (float)positions.Count; + float multiplier = MathHelper.Lerp(0, 1, t); + Color color = Color.Red * multiplier; + var pos = positions[i]; + pos.Y = -pos.Y; + var size = new Vector2(100); + GUI.DrawRectangle(spriteBatch, pos - size / 2, size, color, thickness: 10); + if (i < positions.Count - 1) + { + var nextPos = positions[i + 1]; + nextPos.Y = -nextPos.Y; + GUI.DrawLine(spriteBatch, pos, nextPos, color, width: 10); + } + } + } + foreach (var rects in blockedRects.Values) + { + foreach (var rect in rects) + { + Rectangle newRect = rect; + newRect.Y = -newRect.Y; + GUI.DrawRectangle(spriteBatch, newRect, Color.Red, thickness: 5); + } + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs index e8ca4f341..fdc5a0afe 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObject.cs @@ -126,7 +126,7 @@ namespace Barotrauma int j = 0; foreach (XElement subElement in Prefab.Config.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "deformablesprite") continue; + if (!subElement.Name.ToString().Equals("deformablesprite", StringComparison.OrdinalIgnoreCase)) { continue; } foreach (XElement animationElement in subElement.Elements()) { var newDeformation = SpriteDeformation.Load(animationElement, Prefab.Name); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs index e2d82ee47..dd66c27c5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelObjects/LevelObjectPrefab.cs @@ -161,7 +161,7 @@ namespace Barotrauma bool elementFound = false; foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() == "overridecommonness" + if (subElement.Name.ToString().Equals("overridecommonness", System.StringComparison.OrdinalIgnoreCase) && subElement.GetAttributeString("leveltype", "") == overrideCommonness.Key) { subElement.Attribute("commonness").Value = overrideCommonness.Value.ToString("G", CultureInfo.InvariantCulture); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs index 07ac14679..55a24802c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/LevelRenderer.cs @@ -228,7 +228,7 @@ namespace Barotrauma public void Draw(SpriteBatch spriteBatch, Camera cam) { - if (GameMain.DebugDraw && cam.Zoom > 0.05f) + if (GameMain.DebugDraw && cam.Zoom > 0.1f) { var cells = level.GetCells(cam.WorldViewCenter, 2); foreach (VoronoiCell cell in cells) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs index 7913a822c..f837b4bad 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Levels/WaterRenderer.cs @@ -91,11 +91,11 @@ namespace Barotrauma public void RenderWater(SpriteBatch spriteBatch, RenderTarget2D texture, Camera cam) { spriteBatch.GraphicsDevice.BlendState = BlendState.NonPremultiplied; - + WaterEffect.Parameters["xTexture"].SetValue(texture); Vector2 distortionStrength = cam == null ? DistortionStrength : DistortionStrength * cam.Zoom; - WaterEffect.Parameters["xWaveWidth"].SetValue(DistortionStrength.X); - WaterEffect.Parameters["xWaveHeight"].SetValue(DistortionStrength.Y); + WaterEffect.Parameters["xWaveWidth"].SetValue(distortionStrength.X); + WaterEffect.Parameters["xWaveHeight"].SetValue(distortionStrength.Y); if (BlurAmount > 0.0f) { WaterEffect.CurrentTechnique = WaterEffect.Techniques["WaterShaderBlurred"]; @@ -111,6 +111,9 @@ namespace Barotrauma offset += (cam.Position - new Vector2(cam.WorldView.Width / 2.0f, -cam.WorldView.Height / 2.0f)); offset.Y += cam.WorldView.Height; offset.X += cam.WorldView.Width; +#if LINUX || OSX + offset.X += cam.WorldView.Width; +#endif offset *= DistortionScale; } offset.Y = -offset.Y; @@ -176,6 +179,9 @@ namespace Barotrauma spriteBatch.GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, subVerts.Value, 0, PositionInIndoorsBuffer[subVerts.Key] / 3); } + + WaterEffect.Parameters["xTexture"].SetValue((Texture2D)null); + WaterEffect.CurrentTechnique.Passes[0].Apply(); } public void ScrollWater(Vector2 vel, float deltaTime) @@ -195,7 +201,10 @@ namespace Barotrauma basicEffect.CurrentTechnique.Passes[0].Apply(); graphicsDevice.SamplerStates[0] = SamplerState.PointWrap; - graphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, vertices, 0, PositionInBuffer / 3); + graphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, vertices, 0, PositionInBuffer / 3); + + basicEffect.Texture = null; + basicEffect.CurrentTechnique.Passes[0].Apply(); } public void ResetBuffers() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs index 803b28f82..e0ed2a224 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs @@ -229,7 +229,16 @@ namespace Barotrauma.Lights light.Position = light.ParentBody.DrawPosition; if (light.ParentSub != null) { light.Position -= light.ParentSub.DrawPosition; } } - if (!MathUtils.CircleIntersectsRectangle(light.WorldPosition, light.LightSourceParams.TextureRange, viewRect)) { continue; } + + float range = light.LightSourceParams.TextureRange; + if (light.LightSprite != null) + { + float spriteRange = Math.Max( + light.LightSprite.size.X * light.SpriteScale.X * (0.5f + Math.Abs(light.LightSprite.RelativeOrigin.X - 0.5f)), + light.LightSprite.size.Y * light.SpriteScale.Y * (0.5f + Math.Abs(light.LightSprite.RelativeOrigin.Y - 0.5f))); + range = Math.Max(spriteRange, range); + } + if (!MathUtils.CircleIntersectsRectangle(light.WorldPosition, range, viewRect)) { continue; } activeLights.Add(light); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs index fe2abefaf..8a9114105 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs @@ -977,7 +977,7 @@ namespace Barotrauma.Lights origin, -Rotation, SpriteScale, LightSpriteEffect); } - if (GameMain.DebugDraw) + if (GameMain.DebugDraw && Screen.Selected.Cam.Zoom > 0.1f) { Vector2 drawPos = position; if (ParentSub != null) { drawPos += ParentSub.DrawPosition; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs index 9cc0bdda8..a615e62fa 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/LinkedSubmarine.cs @@ -13,11 +13,28 @@ namespace Barotrauma public override void Draw(SpriteBatch spriteBatch, bool editing, bool back = true) { if (!editing || wallVertices == null) { return; } + + Draw(spriteBatch, Position); - Color color = IsHighlighted ? GUI.Style.Orange : GUI.Style.Green; + if (!Item.ShowLinks) { return; } + + foreach (MapEntity e in linkedTo) + { + bool isLinkAllowed = e is Item item && item.HasTag("dock"); + + GUI.DrawLine(spriteBatch, + new Vector2(WorldPosition.X, -WorldPosition.Y), + new Vector2(e.WorldPosition.X, -e.WorldPosition.Y), + isLinkAllowed ? GUI.Style.Green * 0.5f : GUI.Style.Red * 0.5f, width: 3); + } + } + + public void Draw(SpriteBatch spriteBatch, Vector2 drawPos, float alpha = 1.0f) + { + Color color = (IsHighlighted) ? GUI.Style.Orange : GUI.Style.Green; if (IsSelected) { color = GUI.Style.Red; } - Vector2 pos = Position; + Vector2 pos = drawPos; for (int i = 0; i < wallVertices.Count; i++) { @@ -28,26 +45,14 @@ namespace Barotrauma endPos.Y = -endPos.Y; GUI.DrawLine(spriteBatch, - startPos, - endPos, - color, 0.0f, 5); + startPos, + endPos, + color * alpha, 0.0f, 5); } pos.Y = -pos.Y; - GUI.DrawLine(spriteBatch, pos + Vector2.UnitY * 50.0f, pos - Vector2.UnitY * 50.0f, color, 0.0f, 5); - GUI.DrawLine(spriteBatch, pos + Vector2.UnitX * 50.0f, pos - Vector2.UnitX * 50.0f, color, 0.0f, 5); - - if (!Item.ShowLinks) { return; } - - foreach (MapEntity e in linkedTo) - { - bool isLinkAllowed = e is Item item && item.HasTag("dock"); - - GUI.DrawLine(spriteBatch, - new Vector2(WorldPosition.X, -WorldPosition.Y), - new Vector2(e.WorldPosition.X, -e.WorldPosition.Y), - isLinkAllowed ? GUI.Style.Green * 0.5f : GUI.Style.Red * 0.5f, width: 3); - } + GUI.DrawLine(spriteBatch, pos + Vector2.UnitY * 50.0f, pos - Vector2.UnitY * 50.0f, color * alpha, 0.0f, 5); + GUI.DrawLine(spriteBatch, pos + Vector2.UnitX * 50.0f, pos - Vector2.UnitX * 50.0f, color * alpha, 0.0f, 5); } public override void UpdateEditing(Camera cam) @@ -100,10 +105,9 @@ namespace Barotrauma } var pathContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), isHorizontal: true); - var pathBox = new GUITextBox(new RectTransform(new Vector2(0.75f, 1.0f), pathContainer.RectTransform), filePath, font: GUI.SmallFont); var reloadButton = new GUIButton(new RectTransform(new Vector2(0.25f / pathBox.RectTransform.RelativeSize.X, 1.0f), pathBox.RectTransform, Anchor.CenterRight, Pivot.CenterLeft), - TextManager.Get("ReloadLinkedSub"), style: "GUIButtonSmall") + TextManager.Get("ReloadLinkedSub"), style: "GUIButtonSmall") { OnClicked = Reload, UserData = pathBox, @@ -131,8 +135,9 @@ namespace Barotrauma return false; } - XDocument doc = Submarine.OpenFile(pathBox.Text); + XDocument doc = SubmarineInfo.OpenFile(pathBox.Text); if (doc == null || doc.Root == null) return false; + doc.Root.SetAttributeValue("filepath", pathBox.Text); pathBox.Flash(GUI.Style.Green); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs index f485e4d9c..7f244e92f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntity.cs @@ -16,8 +16,15 @@ namespace Barotrauma private static Vector2 startMovingPos = Vector2.Zero; + private static float keyDelay; + public static Vector2 StartMovingPos => startMovingPos; + // Quick undo/redo for size and movement only. TODO: Remove if we do a more general implementation. + private Memento rectMemento; + + public event Action Resized; + private static bool resizing; private int resizeDirX, resizeDirY; @@ -127,7 +134,7 @@ namespace Barotrauma if (highlightedListBox == null || (GUI.MouseOn != highlightedListBox && !highlightedListBox.IsParentOf(GUI.MouseOn))) { - UpdateHighlightedListBox(null); + UpdateHighlightedListBox(null, false); return; } } @@ -142,11 +149,14 @@ namespace Barotrauma { if (PlayerInput.KeyDown(Keys.Delete)) { - selectedList.ForEach(e => e.Remove()); + selectedList.ForEach(e => + { + e.Remove(); + }); selectedList.Clear(); } - if (PlayerInput.KeyDown(Keys.LeftControl) || PlayerInput.KeyDown(Keys.RightControl)) + if (PlayerInput.IsCtrlDown()) { if (PlayerInput.KeyHit(Keys.C)) { @@ -239,32 +249,7 @@ namespace Barotrauma } } - if (PlayerInput.MouseSpeed.LengthSquared() > 10) - { - highlightTimer = 0.0f; - } - else - { - bool mouseNearHighlightBox = false; - - if (highlightedListBox != null) - { - Rectangle expandedRect = highlightedListBox.Rect; - expandedRect.Inflate(20, 20); - mouseNearHighlightBox = expandedRect.Contains(PlayerInput.MousePosition); - if (!mouseNearHighlightBox) highlightedListBox = null; - } - - highlightTimer += (float)Timing.Step; - if (highlightTimer > 1.0f) - { - if (!mouseNearHighlightBox) - { - UpdateHighlightedListBox(highlightedEntities); - highlightTimer = 0.0f; - } - } - } + UpdateHighlighting(highlightedEntities); } if (highLightedEntity != null) highLightedEntity.isHighlighted = true; @@ -272,35 +257,65 @@ namespace Barotrauma if (GUI.KeyboardDispatcher.Subscriber == null) { + int up = PlayerInput.KeyDown(Keys.Up) ? 1 : 0, + down = PlayerInput.KeyDown(Keys.Down) ? -1 : 0, + left = PlayerInput.KeyDown(Keys.Left) ? -1 : 0, + right = PlayerInput.KeyDown(Keys.Right) ? 1 : 0; + + int xKeysDown = (left + right); + int yKeysDown = (up + down); + + if (xKeysDown != 0 || yKeysDown != 0) { keyDelay += (float) Timing.Step; } else { keyDelay = 0; } + Vector2 nudgeAmount = Vector2.Zero; - if (PlayerInput.KeyHit(Keys.Up)) nudgeAmount.Y = 1f; + + if (keyDelay >= 0.5f) + { + nudgeAmount.Y = yKeysDown; + nudgeAmount.X = xKeysDown; + } + + if (PlayerInput.KeyHit(Keys.Up)) nudgeAmount.Y = 1f; if (PlayerInput.KeyHit(Keys.Down)) nudgeAmount.Y = -1f; if (PlayerInput.KeyHit(Keys.Left)) nudgeAmount.X = -1f; - if (PlayerInput.KeyHit(Keys.Right)) nudgeAmount.X = 1f; + if (PlayerInput.KeyHit(Keys.Right)) nudgeAmount.X = 1f; if (nudgeAmount != Vector2.Zero) { - foreach (MapEntity entityToNudge in selectedList) - { - entityToNudge.Move(nudgeAmount); - } + foreach (MapEntity entityToNudge in selectedList) { entityToNudge.Move(nudgeAmount); } } } + else + { + keyDelay = 0; + } + + bool isShiftDown = PlayerInput.IsShiftDown(); //started moving selected entities if (startMovingPos != Vector2.Zero) { + Item targetContainer = GetPotentialContainer(position, selectedList); + + if (targetContainer != null) { targetContainer.IsHighlighted = true; } + if (PlayerInput.PrimaryMouseButtonReleased()) { //mouse released -> move the entities to the new position of the mouse Vector2 moveAmount = position - startMovingPos; - moveAmount.X = (float)(moveAmount.X > 0.0f ? Math.Floor(moveAmount.X / Submarine.GridSize.X) : Math.Ceiling(moveAmount.X / Submarine.GridSize.X)) * Submarine.GridSize.X; - moveAmount.Y = (float)(moveAmount.Y > 0.0f ? Math.Floor(moveAmount.Y / Submarine.GridSize.Y) : Math.Ceiling(moveAmount.Y / Submarine.GridSize.Y)) * Submarine.GridSize.Y; - if (Math.Abs(moveAmount.X) >= Submarine.GridSize.X || Math.Abs(moveAmount.Y) >= Submarine.GridSize.Y) + + if (!isShiftDown) { - moveAmount = Submarine.VectorToWorldGrid(moveAmount); + moveAmount.X = (float)(moveAmount.X > 0.0f ? Math.Floor(moveAmount.X / Submarine.GridSize.X) : Math.Ceiling(moveAmount.X / Submarine.GridSize.X)) * Submarine.GridSize.X; + moveAmount.Y = (float)(moveAmount.Y > 0.0f ? Math.Floor(moveAmount.Y / Submarine.GridSize.Y) : Math.Ceiling(moveAmount.Y / Submarine.GridSize.Y)) * Submarine.GridSize.Y; + } + + if (Math.Abs(moveAmount.X) >= Submarine.GridSize.X || Math.Abs(moveAmount.Y) >= Submarine.GridSize.Y || isShiftDown) + { + if (!isShiftDown) { moveAmount = Submarine.VectorToWorldGrid(moveAmount); } + //clone - if (PlayerInput.KeyDown(Keys.LeftControl) || PlayerInput.KeyDown(Keys.RightControl)) + if (PlayerInput.IsCtrlDown()) { var clones = Clone(selectedList); selectedList = clones; @@ -308,6 +323,7 @@ namespace Barotrauma } else // move { + List deposited = new List(); foreach (MapEntity e in selectedList) { if (e.rectMemento == null) @@ -316,8 +332,23 @@ namespace Barotrauma e.rectMemento.Store(e.Rect); } e.Move(moveAmount); + + if (isShiftDown && e is Item item && targetContainer != null) + { + if (targetContainer.OwnInventory.TryPutItem(item, Character.Controlled)) + { + GUI.PlayUISound(GUISoundType.DropItem); + deposited.Add(item); + } + else + { + GUI.PlayUISound(GUISoundType.PickItemFail); + } + } e.rectMemento.Store(e.Rect); } + + deposited.ForEach(entity => { selectedList.Remove(entity); }); } } startMovingPos = Vector2.Zero; @@ -352,8 +383,7 @@ namespace Barotrauma if (PlayerInput.PrimaryMouseButtonReleased()) { - if (PlayerInput.KeyDown(Keys.LeftControl) || - PlayerInput.KeyDown(Keys.RightControl)) + if (PlayerInput.IsCtrlDown()) { foreach (MapEntity e in newSelection) { @@ -436,7 +466,84 @@ namespace Barotrauma } } - private static void UpdateHighlightedListBox(List highlightedEntities) + public static Item GetPotentialContainer(Vector2 position, List entities = null) + { + Item targetContainer = null; + bool isShiftDown = PlayerInput.IsShiftDown(); + + if (!isShiftDown) return null; + + foreach (MapEntity e in mapEntityList) + { + if (!e.SelectableInEditor ||!(e is Item potentialContainer)) { continue; } + + if (e.IsMouseOn(position)) + { + if (entities == null) + { + if (potentialContainer.OwnInventory != null && potentialContainer.ParentInventory == null && !potentialContainer.OwnInventory.IsFull()) + { + targetContainer = potentialContainer; + break; + } + } + else + { + foreach (MapEntity selectedEntity in entities) + { + if (!(selectedEntity is Item selectedItem)) { continue; } + if (potentialContainer.OwnInventory != null && potentialContainer.ParentInventory == null && potentialContainer != selectedItem && + potentialContainer.OwnInventory.CanBePut(selectedItem)) + { + targetContainer = potentialContainer; + break; + } + } + } + } + if (targetContainer != null) { break; } + } + + return targetContainer; + } + + /// + /// Updates the logic that runs the highlight box when the mouse is sitting still. + /// + /// + /// + /// true to give items tooltip showing their connection + public static void UpdateHighlighting(List highlightedEntities, bool wiringMode = false) + { + if (PlayerInput.MouseSpeed.LengthSquared() > 10) + { + highlightTimer = 0.0f; + } + else + { + bool mouseNearHighlightBox = false; + + if (highlightedListBox != null) + { + Rectangle expandedRect = highlightedListBox.Rect; + expandedRect.Inflate(20, 20); + mouseNearHighlightBox = expandedRect.Contains(PlayerInput.MousePosition); + if (!mouseNearHighlightBox) highlightedListBox = null; + } + + highlightTimer += (float)Timing.Step; + if (highlightTimer > 1.0f) + { + if (!mouseNearHighlightBox) + { + UpdateHighlightedListBox(highlightedEntities, wiringMode); + highlightTimer = 0.0f; + } + } + } + } + + private static void UpdateHighlightedListBox(List highlightedEntities, bool wiringMode) { if (highlightedEntities == null || highlightedEntities.Count < 2) { @@ -453,14 +560,37 @@ namespace Barotrauma highlightedListBox = new GUIListBox(new RectTransform(new Point(180, highlightedEntities.Count * 18 + 5), GUI.Canvas) { + MaxSize = new Point(int.MaxValue, 256), ScreenSpaceOffset = PlayerInput.MousePosition.ToPoint() + new Point(15) }, style: "GUIToolTip"); foreach (MapEntity entity in highlightedEntities) { - var textBlock = new GUITextBlock(new RectTransform(new Point(highlightedListBox.Content.Rect.Width, 15), highlightedListBox.Content.RectTransform), - ToolBox.LimitString(entity.Name, GUI.SmallFont, 140), font: GUI.SmallFont) + var tooltip = string.Empty; + + if (wiringMode && entity is Item item) { + var wire = item.GetComponent(); + if (wire?.Connections != null) + { + for (var i = 0; i < wire.Connections.Length; i++) + { + var conn = wire.Connections[i]; + if (conn != null) + { + string[] tags = { "[item]", "[pin]" }; + string[] values = { conn.Item?.Name, conn.Name }; + tooltip += TextManager.GetWithVariables("wirelistformat",tags , values); + } + if (i != wire.Connections.Length - 1) { tooltip += '\n'; } + } + } + } + + var textBlock = new GUITextBlock(new RectTransform(new Point(highlightedListBox.Content.Rect.Width, 15), highlightedListBox.Content.RectTransform), + ToolBox.LimitString(entity.Name, GUI.SmallFont, 140), font: GUI.SmallFont) + { + ToolTip = tooltip, UserData = entity }; } @@ -469,8 +599,7 @@ namespace Barotrauma { MapEntity entity = obj as MapEntity; - if (PlayerInput.KeyDown(Keys.LeftControl) || - PlayerInput.KeyDown(Keys.RightControl)) + if (PlayerInput.IsCtrlDown() && !wiringMode) { if (selectedList.Contains(entity)) { @@ -480,11 +609,10 @@ namespace Barotrauma { AddSelection(entity); } + + return true; } - else - { - SelectEntity(entity); - } + SelectEntity(entity); return true; }; @@ -553,6 +681,10 @@ namespace Barotrauma { item.UpdateSpriteStates(deltaTime); } + else if (me is Structure structure) + { + structure.UpdateSpriteStates(deltaTime); + } } } @@ -570,24 +702,52 @@ namespace Barotrauma { Vector2 moveAmount = position - startMovingPos; moveAmount.Y = -moveAmount.Y; - moveAmount.X = (float)(moveAmount.X > 0.0f ? Math.Floor(moveAmount.X / Submarine.GridSize.X) : Math.Ceiling(moveAmount.X / Submarine.GridSize.X)) * Submarine.GridSize.X; - moveAmount.Y = (float)(moveAmount.Y > 0.0f ? Math.Floor(moveAmount.Y / Submarine.GridSize.Y) : Math.Ceiling(moveAmount.Y / Submarine.GridSize.Y)) * Submarine.GridSize.Y; + + bool isShiftDown = PlayerInput.IsShiftDown(); + + if (!isShiftDown) + { + moveAmount.X = (float)(moveAmount.X > 0.0f ? Math.Floor(moveAmount.X / Submarine.GridSize.X) : Math.Ceiling(moveAmount.X / Submarine.GridSize.X)) * Submarine.GridSize.X; + moveAmount.Y = (float)(moveAmount.Y > 0.0f ? Math.Floor(moveAmount.Y / Submarine.GridSize.Y) : Math.Ceiling(moveAmount.Y / Submarine.GridSize.Y)) * Submarine.GridSize.Y; + } //started moving the selected entities - if (Math.Abs(moveAmount.X) >= Submarine.GridSize.X || Math.Abs(moveAmount.Y) >= Submarine.GridSize.Y) + if (Math.Abs(moveAmount.X) >= Submarine.GridSize.X || Math.Abs(moveAmount.Y) >= Submarine.GridSize.Y || isShiftDown) { foreach (MapEntity e in selectedList) { SpriteEffects spriteEffects = SpriteEffects.None; - if (e is Item item) + switch (e) { - if (item.FlippedX && item.Prefab.CanSpriteFlipX) spriteEffects ^= SpriteEffects.FlipHorizontally; - if (item.flippedY && item.Prefab.CanSpriteFlipY) spriteEffects ^= SpriteEffects.FlipVertically; - } - else if (e is Structure structure) - { - if (structure.FlippedX && structure.Prefab.CanSpriteFlipX) spriteEffects ^= SpriteEffects.FlipHorizontally; - if (structure.flippedY && structure.Prefab.CanSpriteFlipY) spriteEffects ^= SpriteEffects.FlipVertically; + case Item item: + { + if (item.FlippedX && item.Prefab.CanSpriteFlipX) spriteEffects ^= SpriteEffects.FlipHorizontally; + if (item.flippedY && item.Prefab.CanSpriteFlipY) spriteEffects ^= SpriteEffects.FlipVertically; + break; + } + case Structure structure: + { + if (structure.FlippedX && structure.Prefab.CanSpriteFlipX) spriteEffects ^= SpriteEffects.FlipHorizontally; + if (structure.flippedY && structure.Prefab.CanSpriteFlipY) spriteEffects ^= SpriteEffects.FlipVertically; + break; + } + case WayPoint wayPoint: + { + Vector2 drawPos = e.WorldPosition; + drawPos.Y = -drawPos.Y; + drawPos += moveAmount; + wayPoint.Draw(spriteBatch, drawPos); + continue; + } + case LinkedSubmarine linkedSub: + { + var ma = moveAmount; + ma.Y = -ma.Y; + Vector2 lPos = linkedSub.Position; + lPos += ma; + linkedSub.Draw(spriteBatch, lPos, alpha: 0.5f); + break; + } } e.prefab?.DrawPlacing(spriteBatch, new Rectangle(e.WorldRect.Location + new Point((int)moveAmount.X, (int)-moveAmount.Y), e.WorldRect.Size), e.Scale, spriteEffects); @@ -643,7 +803,7 @@ namespace Barotrauma } } - if ((PlayerInput.KeyDown(Keys.LeftControl) || PlayerInput.KeyDown(Keys.RightControl))) + if (PlayerInput.IsCtrlDown()) { if (PlayerInput.KeyHit(Keys.N)) { @@ -721,7 +881,10 @@ namespace Barotrauma CopyEntities(entities); - entities.ForEach(e => e.Remove()); + entities.ForEach(e => + { + e.Remove(); + }); entities.Clear(); } @@ -834,6 +997,7 @@ namespace Barotrauma resizeDirX = x; resizeDirY = y; resizing = true; + startMovingPos = Vector2.Zero; } } } @@ -851,6 +1015,11 @@ namespace Barotrauma Vector2 mousePos = Submarine.MouseToWorldGrid(cam, Submarine.MainSub); + if (PlayerInput.IsShiftDown()) + { + mousePos = cam.ScreenToWorld(PlayerInput.MousePosition); + } + if (resizeDirX > 0) { mousePos.X = Math.Max(mousePos.X, rect.X + Submarine.GridSize.X); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs index 676a56d2d..640cd4a95 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/MapEntityPrefab.cs @@ -8,6 +8,12 @@ namespace Barotrauma { public virtual void UpdatePlacing(Camera cam) { + if (PlayerInput.SecondaryMouseButtonClicked()) + { + selected = null; + return; + } + Vector2 placeSize = Submarine.GridSize; if (placePosition == Vector2.Zero) @@ -41,12 +47,6 @@ namespace Barotrauma newRect.Y = -newRect.Y; } - - if (PlayerInput.SecondaryMouseButtonHeld()) - { - placePosition = Vector2.Zero; - selected = null; - } } public virtual void DrawPlacing(SpriteBatch spriteBatch, Camera cam) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs index 10d8107a2..71de94cc1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs @@ -18,11 +18,13 @@ namespace Barotrauma private List convexHulls; + private readonly Dictionary spriteAnimState = new Dictionary(); + public override bool SelectableInEditor { get { - return HasBody ? ShowWalls : ShowStructures;; + return HasBody ? ShowWalls : ShowStructures; } } @@ -38,6 +40,14 @@ namespace Barotrauma { Prefab.sprite?.EnsureLazyLoaded(); Prefab.BackgroundSprite?.EnsureLazyLoaded(); + + foreach (var decorativeSprite in Prefab.DecorativeSprites) + { + decorativeSprite.Sprite.EnsureLazyLoaded(); + spriteAnimState.Add(decorativeSprite, new DecorativeSprite.State()); + } + + UpdateSpriteStates(0.0f); } partial void CreateConvexHull(Vector2 position, Vector2 size, float rotation) @@ -156,19 +166,19 @@ namespace Barotrauma { Rectangle worldRect = WorldRect; - if (worldRect.X > worldView.Right || worldRect.Right < worldView.X) return false; - if (worldRect.Y < worldView.Y - worldView.Height || worldRect.Y - worldRect.Height > worldView.Y) return false; + if (worldRect.X > worldView.Right || worldRect.Right < worldView.X) { return false; } + if (worldRect.Y < worldView.Y - worldView.Height || worldRect.Y - worldRect.Height > worldView.Y) { return false; } return true; } public override void Draw(SpriteBatch spriteBatch, bool editing, bool back = true) { - if (prefab.sprite == null) return; + if (prefab.sprite == null) { return; } if (editing) { - if (!HasBody && !ShowStructures) return; - if (HasBody && !ShowWalls) return; + if (!HasBody && !ShowStructures) { return; } + if (HasBody && !ShowWalls) { return; } } Draw(spriteBatch, editing, back, null); @@ -188,12 +198,13 @@ namespace Barotrauma private void Draw(SpriteBatch spriteBatch, bool editing, bool back = true, Effect damageEffect = null) { - if (prefab.sprite == null) return; + if (prefab.sprite == null) { return; } if (editing) { - if (!HasBody && !ShowStructures) return; - if (HasBody && !ShowWalls) return; + if (!HasBody && !ShowStructures) { return; } + if (HasBody && !ShowWalls) { return; } } + else if (HiddenInGame) { return; } Color color = IsHighlighted ? GUI.Style.Orange : spriteColor; if (IsSelected && editing) @@ -254,7 +265,7 @@ namespace Barotrauma spriteBatch, new Vector2(rect.X + drawOffset.X, -(rect.Y + drawOffset.Y)), new Vector2(rect.Width, rect.Height), - color: color, + color: Prefab.BackgroundSpriteColor, textureScale: TextureScale * Scale, startOffset: backGroundOffset, depth: Math.Max(Prefab.BackgroundSprite.Depth + (ID % 255) * 0.000001f, depth + 0.000001f)); @@ -318,10 +329,20 @@ namespace Barotrauma depth: depth, textureScale: TextureScale * Scale); } + + foreach (var decorativeSprite in Prefab.DecorativeSprites) + { + if (!spriteAnimState[decorativeSprite].IsActive) { continue; } + float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState); + Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState) * Scale; + decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color, + rotation, decorativeSprite.Scale * Scale, prefab.sprite.effects, + depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - prefab.sprite.Depth), 0.999f)); + } prefab.sprite.effects = oldEffects; } - if (GameMain.DebugDraw) + if (GameMain.DebugDraw && Screen.Selected.Cam.Zoom > 0.5f) { if (Bodies != null) { @@ -353,12 +374,70 @@ namespace Barotrauma } } + public void UpdateSpriteStates(float deltaTime) + { + 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++) + { + var decorativeSprite = Prefab.DecorativeSpriteGroups[spriteGroup][i]; + if (decorativeSprite == null) { continue; } + if (spriteGroup > 0) + { + int activeSpriteIndex = ID % Prefab.DecorativeSpriteGroups[spriteGroup].Count; + if (i != activeSpriteIndex) + { + spriteAnimState[decorativeSprite].IsActive = false; + continue; + } + } + + //check if the sprite is active (whether it should be drawn or not) + var spriteState = spriteAnimState[decorativeSprite]; + spriteState.IsActive = true; + foreach (PropertyConditional conditional in decorativeSprite.IsActiveConditionals) + { + if (!ConditionalMatches(conditional)) + { + spriteState.IsActive = false; + break; + } + } + if (!spriteState.IsActive) { continue; } + + //check if the sprite should be animated + bool animate = true; + foreach (PropertyConditional conditional in decorativeSprite.AnimationConditionals) + { + if (!ConditionalMatches(conditional)) { animate = false; break; } + } + if (!animate) { continue; } + spriteState.OffsetState += deltaTime; + spriteState.RotationState += deltaTime; + } + } + } + + private bool ConditionalMatches(PropertyConditional conditional) + { + if (!string.IsNullOrEmpty(conditional.TargetItemComponentName)) + { + return false; + } + else + { + if (!conditional.Matches(this)) { return false; } + } + return true; + } + public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { byte sectionCount = msg.ReadByte(); if (sectionCount != Sections.Length) { - string errorMsg = $"Error while reading a network event for the structure \"{Name}\". Section count does not match (server: {sectionCount} client: {Sections.Length})"; + string errorMsg = $"Error while reading a network event for the structure \"{Name} ({ID})\". Section count does not match (server: {sectionCount} client: {Sections.Length})"; DebugConsole.NewMessage(errorMsg, Color.Red); GameAnalyticsManager.AddErrorEventOnce("Structure.ClientRead:SectionCountMismatch", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs index 34b5a4d12..52961c361 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/StructurePrefab.cs @@ -1,13 +1,29 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; +using System.Collections.Generic; namespace Barotrauma { partial class StructurePrefab : MapEntityPrefab { + public Color BackgroundSpriteColor + { + get; + private set; + } + + public List DecorativeSprites = new List(); + public Dictionary> DecorativeSpriteGroups = new Dictionary>(); + public override void UpdatePlacing(Camera cam) { + if (PlayerInput.SecondaryMouseButtonClicked()) + { + selected = null; + return; + } + Vector2 position = Submarine.MouseToWorldGrid(cam, Submarine.MainSub); Vector2 size = ScaledSize; Rectangle newRect = new Rectangle((int)position.X, (int)position.Y, (int)size.X, (int)size.Y); @@ -40,7 +56,7 @@ namespace Barotrauma if (PlayerInput.PrimaryMouseButtonReleased()) { newRect.Location -= MathUtils.ToPoint(Submarine.MainSub.Position); - var structure = new Structure(newRect, this, Submarine.MainSub) + new Structure(newRect, this, Submarine.MainSub) { Submarine = Submarine.MainSub }; @@ -49,8 +65,6 @@ namespace Barotrauma return; } } - - if (PlayerInput.SecondaryMouseButtonHeld()) selected = null; } public override void DrawPlacing(SpriteBatch spriteBatch, Camera cam) @@ -60,9 +74,6 @@ namespace Barotrauma if (placePosition == Vector2.Zero) { - if (PlayerInput.PrimaryMouseButtonHeld()) - placePosition = Submarine.MouseToWorldGrid(cam, Submarine.MainSub); - newRect.X = (int)position.X; newRect.Y = (int)position.Y; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs index dea60660b..31b597590 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Submarine.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml.Linq; +using Barotrauma.Items.Components; namespace Barotrauma { @@ -35,7 +36,6 @@ namespace Barotrauma partial class Submarine : Entity, IServerSerializable { - public Sprite PreviewImage; public static Vector2 MouseToWorldGrid(Camera cam, Submarine sub) { Vector2 position = PlayerInput.MousePosition; @@ -56,6 +56,8 @@ namespace Barotrauma private static List roundSounds = null; public static RoundSound LoadRoundSound(XElement element, bool stream = false) { + if (GameMain.SoundManager?.Disabled ?? true) { return null; } + string filename = element.GetAttributeString("file", ""); if (string.IsNullOrEmpty(filename)) filename = element.GetAttributeString("sound", ""); @@ -222,7 +224,7 @@ namespace Barotrauma GUI.DrawRectangle(spriteBatch, worldBorders, Color.White, false, 0, 5); - if (sub.subBody == null || sub.subBody.PositionBuffer.Count < 2) continue; + if (sub.SubBody == null || sub.subBody.PositionBuffer.Count < 2) continue; Vector2 prevPos = ConvertUnits.ToDisplayUnits(sub.subBody.PositionBuffer[0].Position); prevPos.Y = -prevPos.Y; @@ -245,7 +247,7 @@ namespace Barotrauma public static Color DamageEffectColor; private static readonly List depthSortedDamageable = new List(); - public static void DrawDamageable(SpriteBatch spriteBatch, Effect damageEffect, bool editing = false) + public static void DrawDamageable(SpriteBatch spriteBatch, Effect damageEffect, bool editing = false, Predicate predicate = null) { var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.mapEntityList; @@ -256,6 +258,10 @@ namespace Barotrauma { if (e is Structure structure && structure.DrawDamageEffect) { + if (predicate != null) + { + if (!predicate(e)) continue; + } float drawDepth = structure.GetDrawDepth(); int i = 0; while (i < depthSortedDamageable.Count) @@ -329,125 +335,6 @@ namespace Barotrauma } } - public static bool SaveCurrent(string filePath, MemoryStream previewImage = null) - { - if (MainSub == null) - { - MainSub = new Submarine(filePath); - } - - MainSub.filePath = filePath; - return MainSub.SaveAs(filePath, previewImage); - } - - public void CreatePreviewWindow(GUIComponent parent) - { - var content = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform), style: null); - - if (PreviewImage == null) - { - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform), TextManager.Get(SavedSubmarines.Contains(this) ? "SubPreviewImageNotFound" : "SubNotDownloaded")); - } - else - { - var submarinePreviewBackground = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform), style: null) { Color = Color.Black }; - new GUIImage(new RectTransform(new Vector2(0.98f), submarinePreviewBackground.RectTransform, Anchor.Center), PreviewImage, scaleToFit: true); - new GUIFrame(new RectTransform(Vector2.One, submarinePreviewBackground.RectTransform), "InnerGlow", color: Color.Black); - } - var descriptionBox = new GUIListBox(new RectTransform(new Vector2(1, 0.5f), content.RectTransform, Anchor.BottomCenter)) - { - UserData = "descriptionbox", - ScrollBarVisible = true, - Spacing = 5 - }; - - //space - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.03f), descriptionBox.Content.RectTransform), style: null); - - new GUITextBlock(new RectTransform(new Vector2(1, 0), descriptionBox.Content.RectTransform), TextManager.Get("submarine.name." + Name, true) ?? Name, font: GUI.LargeFont, wrap: true) { ForceUpperCase = true, CanBeFocused = false }; - - float leftPanelWidth = 0.6f; - float rightPanelWidth = 0.4f / leftPanelWidth; - - ScalableFont font = descriptionBox.Rect.Width < 350 ? GUI.SmallFont : GUI.Font; - - Vector2 realWorldDimensions = Dimensions * Physics.DisplayToRealWorldRatio; - if (realWorldDimensions != Vector2.Zero) - { - string dimensionsStr = TextManager.GetWithVariables("DimensionsFormat", new string[2] { "[width]", "[height]" }, new string[2] { ((int)realWorldDimensions.X).ToString(), ((int)realWorldDimensions.Y).ToString() }); - - var dimensionsText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), descriptionBox.Content.RectTransform), - TextManager.Get("Dimensions"), textAlignment: Alignment.TopLeft, font: font, wrap: true) - { CanBeFocused = false }; - new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), dimensionsText.RectTransform, Anchor.TopRight, Pivot.TopLeft), - dimensionsStr, textAlignment: Alignment.TopLeft, font: font, wrap: true) - { CanBeFocused = false }; - dimensionsText.RectTransform.MinSize = new Point(0, dimensionsText.Children.First().Rect.Height); - } - - if (RecommendedCrewSizeMax > 0) - { - var crewSizeText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), descriptionBox.Content.RectTransform), - TextManager.Get("RecommendedCrewSize"), textAlignment: Alignment.TopLeft, font: font, wrap: true) - { CanBeFocused = false }; - new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), crewSizeText.RectTransform, Anchor.TopRight, Pivot.TopLeft), - RecommendedCrewSizeMin + " - " + RecommendedCrewSizeMax, textAlignment: Alignment.TopLeft, font: font, wrap: true) - { CanBeFocused = false }; - crewSizeText.RectTransform.MinSize = new Point(0, crewSizeText.Children.First().Rect.Height); - } - - if (!string.IsNullOrEmpty(RecommendedCrewExperience)) - { - var crewExperienceText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), descriptionBox.Content.RectTransform), - TextManager.Get("RecommendedCrewExperience"), textAlignment: Alignment.TopLeft, font: font, wrap: true) - { CanBeFocused = false }; - new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), crewExperienceText.RectTransform, Anchor.TopRight, Pivot.TopLeft), - TextManager.Get(RecommendedCrewExperience), textAlignment: Alignment.TopLeft, font: font, wrap: true) - { CanBeFocused = false }; - crewExperienceText.RectTransform.MinSize = new Point(0, crewExperienceText.Children.First().Rect.Height); - } - - if (RequiredContentPackages.Any()) - { - var contentPackagesText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), descriptionBox.Content.RectTransform), - TextManager.Get("RequiredContentPackages"), textAlignment: Alignment.TopLeft, font: font) - { CanBeFocused = false }; - new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), contentPackagesText.RectTransform, Anchor.TopRight, Pivot.TopLeft), - string.Join(", ", RequiredContentPackages), textAlignment: Alignment.TopLeft, font: font, wrap: true) - { CanBeFocused = false }; - contentPackagesText.RectTransform.MinSize = new Point(0, contentPackagesText.Children.First().Rect.Height); - } - - // show what game version the submarine was created on - if (!IsVanillaSubmarine() && GameVersion != null) - { - var versionText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), descriptionBox.Content.RectTransform), - TextManager.Get("serverlistversion"), textAlignment: Alignment.TopLeft, font: font, wrap: true) - { CanBeFocused = false }; - new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), versionText.RectTransform, Anchor.TopRight, Pivot.TopLeft), - GameVersion.ToString(), textAlignment: Alignment.TopLeft, font: font, wrap: true) - { CanBeFocused = false }; - - versionText.RectTransform.MinSize = new Point(0, versionText.Children.First().Rect.Height); - } - - GUITextBlock.AutoScaleAndNormalize(descriptionBox.Content.Children.Where(c => c is GUITextBlock).Cast()); - - //space - new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), descriptionBox.Content.RectTransform), style: null); - - if (!string.IsNullOrEmpty(Description)) - { - new GUITextBlock(new RectTransform(new Vector2(1, 0), descriptionBox.Content.RectTransform), - TextManager.Get("SaveSubDialogDescription", fallBackTag: "WorkshopItemDescription"), font: GUI.Font, wrap: true) { CanBeFocused = false, ForceUpperCase = true }; - } - - new GUITextBlock(new RectTransform(new Vector2(1, 0), descriptionBox.Content.RectTransform), Description, font: font, wrap: true) - { - CanBeFocused = false - }; - } - public void CreateMiniMap(GUIComponent parent, IEnumerable pointsOfInterest = null) { Rectangle worldBorders = GetDockedBorders(); @@ -496,21 +383,6 @@ namespace Barotrauma } } - public bool IsVanillaSubmarine() - { - var vanilla = GameMain.VanillaContent; - if (vanilla != null) - { - var vanillaSubs = vanilla.GetFilesOfType(ContentType.Submarine); - string pathToCompare = filePath.Replace(@"\", @"/").ToLowerInvariant(); - if (vanillaSubs.Any(sub => sub.Replace(@"\", @"/").ToLowerInvariant() == pathToCompare)) - { - return true; - } - } - return false; - } - public void CheckForErrors() { List errorMsgs = new List(); @@ -531,6 +403,11 @@ namespace Barotrauma } } + if (!WayPoint.WayPointList.Any(wp => wp.ShouldBeSaved && wp.SpawnType == SpawnType.Human)) + { + errorMsgs.Add(TextManager.Get("NoHumanSpawnpointWarning")); + } + if (!WayPoint.WayPointList.Any(wp => wp.ShouldBeSaved && wp.SpawnType == SpawnType.Path)) { errorMsgs.Add(TextManager.Get("NoWaypointsWarning")); @@ -605,7 +482,7 @@ namespace Barotrauma public void ClientRead(ServerNetObject type, IReadMessage msg, float sendingTime) { - var posInfo = PhysicsBody.ClientRead(type, msg, sendingTime, parentDebugName: Name); + var posInfo = PhysicsBody.ClientRead(type, msg, sendingTime, parentDebugName: Info.Name); msg.ReadPadBits(); if (posInfo != null) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs new file mode 100644 index 000000000..b638ec40f --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/SubmarineInfo.cs @@ -0,0 +1,149 @@ +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml.Linq; + +namespace Barotrauma +{ + partial class SubmarineInfo : IDisposable + { + public Sprite PreviewImage; + + partial void InitProjectSpecific() + { + string previewImageData = SubmarineElement.GetAttributeString("previewimage", ""); + if (!string.IsNullOrEmpty(previewImageData)) + { + try + { + using (MemoryStream mem = new MemoryStream(Convert.FromBase64String(previewImageData))) + { + var texture = TextureLoader.FromStream(mem, path: FilePath); + if (texture == null) { throw new Exception("PreviewImage texture returned null"); } + PreviewImage = new Sprite(texture, null, null); + } + } + catch (Exception e) + { + DebugConsole.ThrowError("Loading the preview image of the submarine \"" + Name + "\" failed. The file may be corrupted.", e); + GameAnalyticsManager.AddErrorEventOnce("Submarine..ctor:PreviewImageLoadingFailed", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, + "Loading the preview image of the submarine \"" + Name + "\" failed. The file may be corrupted."); + PreviewImage = null; + } + } + } + + + public void CreatePreviewWindow(GUIComponent parent) + { + var content = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform), style: null); + + if (PreviewImage == null) + { + new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform), TextManager.Get(SavedSubmarines.Contains(this) ? "SubPreviewImageNotFound" : "SubNotDownloaded")); + } + else + { + var submarinePreviewBackground = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform), style: null) { Color = Color.Black }; + new GUIImage(new RectTransform(new Vector2(0.98f), submarinePreviewBackground.RectTransform, Anchor.Center), PreviewImage, scaleToFit: true); + new GUIFrame(new RectTransform(Vector2.One, submarinePreviewBackground.RectTransform), "InnerGlow", color: Color.Black); + } + var descriptionBox = new GUIListBox(new RectTransform(new Vector2(1, 0.5f), content.RectTransform, Anchor.BottomCenter)) + { + UserData = "descriptionbox", + ScrollBarVisible = true, + Spacing = 5 + }; + + //space + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.03f), descriptionBox.Content.RectTransform), style: null); + + new GUITextBlock(new RectTransform(new Vector2(1, 0), descriptionBox.Content.RectTransform), TextManager.Get("submarine.name." + Name, true) ?? Name, font: GUI.LargeFont, wrap: true) { ForceUpperCase = true, CanBeFocused = false }; + + float leftPanelWidth = 0.6f; + float rightPanelWidth = 0.4f / leftPanelWidth; + + ScalableFont font = descriptionBox.Rect.Width < 350 ? GUI.SmallFont : GUI.Font; + + Vector2 realWorldDimensions = Dimensions * Physics.DisplayToRealWorldRatio; + if (realWorldDimensions != Vector2.Zero) + { + string dimensionsStr = TextManager.GetWithVariables("DimensionsFormat", new string[2] { "[width]", "[height]" }, new string[2] { ((int)realWorldDimensions.X).ToString(), ((int)realWorldDimensions.Y).ToString() }); + + var dimensionsText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), descriptionBox.Content.RectTransform), + TextManager.Get("Dimensions"), textAlignment: Alignment.TopLeft, font: font, wrap: true) + { CanBeFocused = false }; + new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), dimensionsText.RectTransform, Anchor.TopRight, Pivot.TopLeft), + dimensionsStr, textAlignment: Alignment.TopLeft, font: font, wrap: true) + { CanBeFocused = false }; + dimensionsText.RectTransform.MinSize = new Point(0, dimensionsText.Children.First().Rect.Height); + } + + if (RecommendedCrewSizeMax > 0) + { + var crewSizeText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), descriptionBox.Content.RectTransform), + TextManager.Get("RecommendedCrewSize"), textAlignment: Alignment.TopLeft, font: font, wrap: true) + { CanBeFocused = false }; + new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), crewSizeText.RectTransform, Anchor.TopRight, Pivot.TopLeft), + RecommendedCrewSizeMin + " - " + RecommendedCrewSizeMax, textAlignment: Alignment.TopLeft, font: font, wrap: true) + { CanBeFocused = false }; + crewSizeText.RectTransform.MinSize = new Point(0, crewSizeText.Children.First().Rect.Height); + } + + if (!string.IsNullOrEmpty(RecommendedCrewExperience)) + { + var crewExperienceText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), descriptionBox.Content.RectTransform), + TextManager.Get("RecommendedCrewExperience"), textAlignment: Alignment.TopLeft, font: font, wrap: true) + { CanBeFocused = false }; + new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), crewExperienceText.RectTransform, Anchor.TopRight, Pivot.TopLeft), + TextManager.Get(RecommendedCrewExperience), textAlignment: Alignment.TopLeft, font: font, wrap: true) + { CanBeFocused = false }; + crewExperienceText.RectTransform.MinSize = new Point(0, crewExperienceText.Children.First().Rect.Height); + } + + if (RequiredContentPackages.Any()) + { + var contentPackagesText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), descriptionBox.Content.RectTransform), + TextManager.Get("RequiredContentPackages"), textAlignment: Alignment.TopLeft, font: font) + { CanBeFocused = false }; + new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), contentPackagesText.RectTransform, Anchor.TopRight, Pivot.TopLeft), + string.Join(", ", RequiredContentPackages), textAlignment: Alignment.TopLeft, font: font, wrap: true) + { CanBeFocused = false }; + contentPackagesText.RectTransform.MinSize = new Point(0, contentPackagesText.Children.First().Rect.Height); + } + + // show what game version the submarine was created on + if (!IsVanillaSubmarine() && GameVersion != null) + { + var versionText = new GUITextBlock(new RectTransform(new Vector2(leftPanelWidth, 0), descriptionBox.Content.RectTransform), + TextManager.Get("serverlistversion"), textAlignment: Alignment.TopLeft, font: font, wrap: true) + { CanBeFocused = false }; + new GUITextBlock(new RectTransform(new Vector2(rightPanelWidth, 0.0f), versionText.RectTransform, Anchor.TopRight, Pivot.TopLeft), + GameVersion.ToString(), textAlignment: Alignment.TopLeft, font: font, wrap: true) + { CanBeFocused = false }; + + versionText.RectTransform.MinSize = new Point(0, versionText.Children.First().Rect.Height); + } + + GUITextBlock.AutoScaleAndNormalize(descriptionBox.Content.Children.Where(c => c is GUITextBlock).Cast()); + + //space + new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), descriptionBox.Content.RectTransform), style: null); + + if (!string.IsNullOrEmpty(Description)) + { + new GUITextBlock(new RectTransform(new Vector2(1, 0), descriptionBox.Content.RectTransform), + TextManager.Get("SaveSubDialogDescription", fallBackTag: "WorkshopItemDescription"), font: GUI.Font, wrap: true) + { CanBeFocused = false, ForceUpperCase = true }; + } + + new GUITextBlock(new RectTransform(new Vector2(1, 0), descriptionBox.Content.RectTransform), Description, font: font, wrap: true) + { + CanBeFocused = false + }; + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs index 195cb57a4..255b39587 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/WayPoint.cs @@ -1,17 +1,16 @@ -using Microsoft.Xna.Framework; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; -using Barotrauma.Items.Components; -using System.Linq; namespace Barotrauma { partial class WayPoint : MapEntity { - private static Texture2D iconTexture; - private const int IconSize = 32; - private static int[] iconIndices = { 3, 0, 1, 2 }; + private static Dictionary iconSprites; + private const int WaypointSize = 12, SpawnPointSize = 32; public override bool IsVisible(Rectangle worldView) { @@ -23,58 +22,58 @@ namespace Barotrauma get { return !IsHidden(); } } + public override void Draw(SpriteBatch spriteBatch, bool editing, bool back = true) { - if (!editing && !GameMain.DebugDraw) { return; } - + if (!editing && (!GameMain.DebugDraw || Screen.Selected.Cam.Zoom < 0.1f)) { return; } if (IsHidden()) { return; } - //Rectangle drawRect = - // Submarine == null ? rect : new Rectangle((int)(Submarine.DrawPosition.X + rect.X), (int)(Submarine.DrawPosition.Y + rect.Y), rect.Width, rect.Height); - Vector2 drawPos = Position; - if (Submarine != null) drawPos += Submarine.DrawPosition; + if (Submarine != null) { drawPos += Submarine.DrawPosition; } drawPos.Y = -drawPos.Y; - Color clr = currentHull == null ? Color.Blue : Color.White; + Draw(spriteBatch, drawPos); + } + + public void Draw(SpriteBatch spriteBatch, Vector2 drawPos) + { + Color clr = currentHull == null ? Color.CadetBlue : GUI.Style.Green; + if (spawnType != SpawnType.Path) { clr = Color.Gray; } if (isObstructed) { clr = Color.Black; } - if (IsSelected) clr = GUI.Style.Red; - if (IsHighlighted) clr = Color.DarkRed; + if (IsHighlighted || IsHighlighted) { clr = Color.Lerp(clr, Color.White, 0.8f); } - int iconX = iconIndices[(int)spawnType] * IconSize % iconTexture.Width; - int iconY = (int)(Math.Floor(iconIndices[(int)spawnType] * IconSize / (float)iconTexture.Width)) * IconSize; + int iconSize = spawnType == SpawnType.Path ? WaypointSize : SpawnPointSize; + if (ConnectedGap != null || Ladders != null || Stairs != null || SpawnType != SpawnType.Path) { iconSize = (int)(iconSize * 1.5f); } - int iconSize = IconSize; - if (ConnectedGap != null) + if (IsSelected || IsHighlighted) { - iconSize = (int)(iconSize * 1.5f); - } - if (Ladders != null) - { - iconSize = (int)(iconSize * 1.5f); - } - if (Stairs != null) - { - iconSize = (int)(iconSize * 1.5f); + int glowSize = (int)(iconSize * 1.5f); + GUI.Style.UIGlowCircular.Draw(spriteBatch, + new Rectangle((int)(drawPos.X - glowSize / 2), (int)(drawPos.Y - glowSize / 2), glowSize, glowSize), + Color.White); } - spriteBatch.Draw(iconTexture, - new Rectangle((int)(drawPos.X - iconSize / 2), (int)(drawPos.Y - iconSize / 2), iconSize, iconSize), - new Rectangle(iconX, iconY, IconSize, IconSize), clr); - - //GUI.DrawRectangle(spriteBatch, new Rectangle(drawRect.X, -drawRect.Y, rect.Width, rect.Height), clr, true); - - //GUI.SmallFont.DrawString(spriteBatch, Position.ToString(), new Vector2(Position.X, -Position.Y), Color.White); + Sprite sprite = iconSprites[SpawnType]; + if (spawnType == SpawnType.Human && AssignedJob?.Icon != null) + { + sprite = iconSprites[SpawnType.Path]; + } + sprite.Draw(spriteBatch, drawPos, clr, scale: iconSize / (float)sprite.SourceRect.Width, depth: 0.001f); + sprite.RelativeOrigin = Vector2.One * 0.5f; + if (spawnType == SpawnType.Human && AssignedJob?.Icon != null) + { + AssignedJob.Icon.Draw(spriteBatch, drawPos, AssignedJob.UIColor, scale: iconSize / (float)AssignedJob.Icon.SourceRect.Width * 0.8f, depth: 0.0f); + } foreach (MapEntity e in linkedTo) { GUI.DrawLine(spriteBatch, drawPos, new Vector2(e.DrawPosition.X, -e.DrawPosition.Y), - isObstructed ? Color.Gray : GUI.Style.Green, width: 5); + (isObstructed ? Color.Gray : GUI.Style.Green) * 0.7f, width: 5, depth: 0.002f); } GUI.SmallFont.DrawString(spriteBatch, @@ -83,6 +82,14 @@ namespace Barotrauma Color.WhiteSmoke); } + public override bool IsMouseOn(Vector2 position) + { + if (IsHidden()) { return false; } + float dist = Vector2.DistanceSquared(position, WorldPosition); + float radius = (SpawnType == SpawnType.Path ? WaypointSize : SpawnPointSize) * 0.6f; + return dist < radius * radius; + } + private bool IsHidden() { if (spawnType == SpawnType.Path) @@ -101,59 +108,72 @@ namespace Barotrauma { editingHUD = CreateEditingHUD(); } - + if (IsSelected && PlayerInput.PrimaryMouseButtonClicked()) { Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition); - // Update gaps, ladders, and stairs - UpdateLinkedEntity(position, Gap.GapList, gap => ConnectedGap = gap, gap => + if (PlayerInput.KeyDown(Keys.Space)) { - if (ConnectedGap == gap) + foreach (MapEntity e in mapEntityList) { - ConnectedGap = null; - } - }); - UpdateLinkedEntity(position, Item.ItemList, i => - { - var ladder = i?.GetComponent(); - if (ladder != null) - { - Ladders = ladder; - } - }, i => - { - var ladder = i?.GetComponent(); - if (ladder != null) - { - if (Ladders == ladder) + if (e.GetType() != typeof(WayPoint)) continue; + if (e == this) continue; + + if (!Submarine.RectContains(e.Rect, position)) continue; + + if (linkedTo.Contains(e)) { - Ladders = null; + linkedTo.Remove(e); + e.linkedTo.Remove(this); + } + else + { + linkedTo.Add(e); + e.linkedTo.Add(this); } } - }, inflate: 5); - // TODO: Cannot check the rectangle, since the rectangle is not rotated -> Need to use the collider. - //var stairList = mapEntityList.Where(me => me is Structure s && s.StairDirection != Direction.None).Select(me => me as Structure); - //UpdateLinkedEntity(position, stairList, s => - //{ - // Stairs = s; - //}, s => - //{ - // if (Stairs == s) - // { - // Stairs = null; - // } - //}); - - foreach (MapEntity e in mapEntityList) + } + else { - if (e.GetType() != typeof(WayPoint)) continue; - if (e == this) continue; - - if (!Submarine.RectContains(e.Rect, position)) continue; - - linkedTo.Add(e); - e.linkedTo.Add(this); + // Update gaps, ladders, and stairs + UpdateLinkedEntity(position, Gap.GapList, gap => ConnectedGap = gap, gap => + { + if (ConnectedGap == gap) + { + ConnectedGap = null; + } + }); + UpdateLinkedEntity(position, Item.ItemList, i => + { + var ladder = i?.GetComponent(); + if (ladder != null) + { + Ladders = ladder; + } + }, i => + { + var ladder = i?.GetComponent(); + if (ladder != null) + { + if (Ladders == ladder) + { + Ladders = null; + } + } + }, inflate: 5); + // TODO: Cannot check the rectangle, since the rectangle is not rotated -> Need to use the collider. + //var stairList = mapEntityList.Where(me => me is Structure s && s.StairDirection != Direction.None).Select(me => me as Structure); + //UpdateLinkedEntity(position, stairList, s => + //{ + // Stairs = s; + //}, s => + //{ + // if (Stairs == s) + // { + // Stairs = null; + // } + //}); } } } @@ -178,14 +198,19 @@ namespace Barotrauma private bool ChangeSpawnType(GUIButton button, object obj) { GUITextBlock spawnTypeText = button.Parent.GetChildByUserData("spawntypetext") as GUITextBlock; - spawnType += (int)button.UserData; - - if (spawnType > SpawnType.Cargo) spawnType = SpawnType.Human; - if (spawnType < SpawnType.Human) spawnType = SpawnType.Cargo; - + var values = Enum.GetValues(typeof(SpawnType)); + int firstIndex = 1; + int lastIndex = values.Length - 1; + if ((int)spawnType > lastIndex) + { + spawnType = (SpawnType)firstIndex; + } + if ((int)spawnType < firstIndex) + { + spawnType = (SpawnType)values.GetValue(lastIndex); + } spawnTypeText.Text = spawnType.ToString(); - return true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs index c0b8257c6..10541b14b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ChatMessage.cs @@ -17,10 +17,12 @@ namespace Barotrauma.Networking { UInt16 ID = msg.ReadUInt16(); ChatMessageType type = (ChatMessageType)msg.ReadByte(); + PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None; string txt = ""; if (type != ChatMessageType.Order) { + changeType = (PlayerConnectionChangeType)msg.ReadByte(); txt = msg.ReadString(); } @@ -114,7 +116,7 @@ namespace Barotrauma.Networking GameMain.Client.ServerSettings.ServerLog?.WriteLine(txt, messageType); break; default: - GameMain.Client.AddChatMessage(txt, type, senderName, senderCharacter); + GameMain.Client.AddChatMessage(txt, type, senderName, senderCharacter, changeType); break; } LastID = ID; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs index 7d0273a68..7a254152f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Client.cs @@ -14,8 +14,10 @@ namespace Barotrauma.Networking public UInt64 SteamID; public byte ID; public UInt16 CharacterID; + public float Karma; public bool Muted; public bool InGame; + public bool HasPermissions; public bool AllowKicking; } @@ -44,6 +46,8 @@ namespace Barotrauma.Networking public bool AllowKicking; + public float Karma; + public void UpdateSoundPosition() { if (VoipSound == null) { return; } @@ -72,7 +76,8 @@ namespace Barotrauma.Networking else { VoipSound.SetPosition(null); - } + VoipSound.Gain = 1.0f; + } } partial void InitProjSpecific() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index 8e74ab80a..06d3641cf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -28,6 +28,8 @@ namespace Barotrauma.Networking get { return name; } } + public string PendingName = string.Empty; + public void SetName(string value) { value = value.Replace(":", "").Replace(";", ""); @@ -78,7 +80,7 @@ namespace Barotrauma.Networking private List otherClients; - private readonly List serverSubmarines = new List(); + private readonly List serverSubmarines = new List(); private string serverIP, serverName; @@ -112,6 +114,7 @@ namespace Barotrauma.Networking public bool SpawnAsTraitor; public string TraitorFirstObjective; + public TraitorMissionPrefab TraitorMission = null; public byte ID { @@ -482,19 +485,24 @@ namespace Barotrauma.Networking if (requiresPw && !canStart && !connectCancelled) { + GUI.ClearCursorWait(); reconnectBox?.Close(); reconnectBox = null; string pwMsg = TextManager.Get("PasswordRequired"); var msgBox = new GUIMessageBox(pwMsg, "", new string[] { TextManager.Get("OK"), TextManager.Get("Cancel") }, - relativeSize: new Vector2(0.25f, 0.1f), minSize: new Point(400, 170)); - var passwordHolder = new GUILayoutGroup(new RectTransform(Vector2.One, msgBox.Content.RectTransform), childAnchor: Anchor.TopCenter); - var passwordBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1f) , passwordHolder.RectTransform) { MinSize = new Point(0, 20) }) + relativeSize: new Vector2(0.25f, 0.1f), minSize: new Point(400, (int)(170 * Math.Max(1.0f, GUI.Scale)))); + var passwordHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), msgBox.Content.RectTransform), childAnchor: Anchor.TopCenter); + var passwordBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1f), passwordHolder.RectTransform) { MinSize = new Point(0, 20) }) { UserData = "password", Censor = true }; + msgBox.Content.Recalculate(); + msgBox.Content.RectTransform.MinSize = new Point(0, msgBox.Content.RectTransform.Children.Sum(c => c.Rect.Height)); + msgBox.Content.Parent.RectTransform.MinSize = new Point(0, (int)(msgBox.Content.RectTransform.MinSize.Y / msgBox.Content.RectTransform.RelativeSize.Y)); + var okButton = msgBox.Buttons[0]; var cancelButton = msgBox.Buttons[1]; @@ -509,6 +517,7 @@ namespace Barotrauma.Networking { requiresPw = false; connectCancelled = true; + GameMain.ServerListScreen.Select(); return true; }; @@ -540,6 +549,7 @@ namespace Barotrauma.Networking foreach (Client c in ConnectedClients) { + if (c.Character != null && c.Character.Removed) { c.Character = null; } c.UpdateSoundPosition(); } @@ -664,6 +674,7 @@ namespace Barotrauma.Networking if (header != ServerPacketHeader.STARTGAMEFINALIZE && header != ServerPacketHeader.ENDGAME && + header != ServerPacketHeader.PING_REQUEST && roundInitStatus == RoundInitStatus.WaitingForStartGameFinalize) { //rewind the header byte we just read @@ -674,6 +685,31 @@ namespace Barotrauma.Networking switch (header) { + case ServerPacketHeader.PING_REQUEST: + IWriteMessage response = new WriteOnlyMessage(); + response.Write((byte)ClientPacketHeader.PING_RESPONSE); + byte requestLen = inc.ReadByte(); + response.Write(requestLen); + for (int i=0;i c.ID == clientId); + if (client != null) + { + client.Ping = clientPing; + } + } + break; case ServerPacketHeader.UPDATE_LOBBY: ReadLobbyUpdate(inc); break; @@ -837,7 +873,7 @@ namespace Barotrauma.Networking if (Level.Loaded.EqualityCheckVal != levelEqualityCheckVal) { string errorMsg = "Level equality check failed. The level generated at your end doesn't match the level generated by the server (seed: " + Level.Loaded.Seed + - ", sub: " + Submarine.MainSub.Name + " (" + Submarine.MainSub.MD5Hash.ShortHash + ")" + + ", sub: " + Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash.ShortHash + ")" + ", mirrored: " + Level.Loaded.Mirrored + ")."; GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:LevelsDontMatch" + Level.Loaded.Seed, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); throw new Exception(errorMsg); @@ -1051,11 +1087,13 @@ namespace Barotrauma.Networking var missionPrefab = TraitorMissionPrefab.List.Find(t => t.Identifier == missionIdentifier); Sprite icon = missionPrefab?.Icon; - switch(messageType) { + switch(messageType) + { case TraitorMessageType.Objective: var isTraitor = !string.IsNullOrEmpty(message); SpawnAsTraitor = isTraitor; TraitorFirstObjective = message; + TraitorMission = missionPrefab; if (Character != null) { Character.IsTraitor = isTraitor; @@ -1326,7 +1364,6 @@ namespace Barotrauma.Networking mirrorLevel: campaign.Map.CurrentLocation != campaign.Map.SelectedConnection.Locations[0]); });*/ GameMain.GameSession.StartRound(campaign.Map.SelectedConnection.Level, - reloadSub: true, mirrorLevel: campaign.Map.CurrentLocation != campaign.Map.SelectedConnection.Locations[0]); } @@ -1406,9 +1443,9 @@ namespace Barotrauma.Networking } } - if (GameMain.GameSession.Submarine.IsFileCorrupted) + if (GameMain.GameSession.Submarine.Info.IsFileCorrupted) { - DebugConsole.ThrowError($"Failed to start a round. Could not load the submarine \"{GameMain.GameSession.Submarine.Name}\"."); + DebugConsole.ThrowError($"Failed to start a round. Could not load the submarine \"{GameMain.GameSession.Submarine.Info.Name}\"."); yield return CoroutineStatus.Failure; } @@ -1498,8 +1535,8 @@ namespace Barotrauma.Networking bool requiredContentPackagesInstalled = inc.ReadBoolean(); var matchingSub = - Submarine.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.Hash == subHash) ?? - new Submarine(Path.Combine(Submarine.SavePath, subName) + ".sub", subHash, false); + SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.Hash == subHash) ?? + new SubmarineInfo(Path.Combine(SubmarineInfo.SavePath, subName) + ".sub", subHash, tryLoad: false); matchingSub.RequiredContentPackagesInstalled = requiredContentPackagesInstalled; serverSubmarines.Add(matchingSub); @@ -1533,9 +1570,11 @@ namespace Barotrauma.Networking string name = inc.ReadString(); string preferredJob = inc.ReadString(); UInt16 characterID = inc.ReadUInt16(); + float karma = inc.ReadSingle(); bool muted = inc.ReadBoolean(); bool inGame = inc.ReadBoolean(); - bool allowKicking = inc.ReadBoolean(); + bool hasPermissions = inc.ReadBoolean(); + bool allowKicking = inc.ReadBoolean() || IsServerOwner; inc.ReadPadBits(); tempClients.Add(new TempClient @@ -1546,8 +1585,10 @@ namespace Barotrauma.Networking Name = name, PreferredJob = preferredJob, CharacterID = characterID, + Karma = karma, Muted = muted, InGame = inGame, + HasPermissions = hasPermissions, AllowKicking = allowKicking }); } @@ -1575,7 +1616,9 @@ namespace Barotrauma.Networking existingClient.NameID = tc.NameID; existingClient.PreferredJob = tc.PreferredJob; existingClient.Character = null; + existingClient.Karma = tc.Karma; existingClient.Muted = tc.Muted; + existingClient.HasPermissions = tc.HasPermissions; existingClient.InGame = tc.InGame; existingClient.AllowKicking = tc.AllowKicking; GameMain.NetLobbyScreen.SetPlayerNameAndJobPreference(existingClient); @@ -2028,15 +2071,15 @@ namespace Barotrauma.Networking { case FileTransferType.Submarine: new GUIMessageBox(TextManager.Get("ServerDownloadFinished"), TextManager.GetWithVariable("FileDownloadedNotification", "[filename]", transfer.FileName)); - var newSub = new Submarine(transfer.FilePath); + var newSub = new SubmarineInfo(transfer.FilePath); if (newSub.IsFileCorrupted) { return; } - var existingSubs = Submarine.SavedSubmarines.Where(s => s.Name == newSub.Name && s.MD5Hash.Hash == newSub.MD5Hash.Hash).ToList(); - foreach (Submarine existingSub in existingSubs) + var existingSubs = SubmarineInfo.SavedSubmarines.Where(s => s.Name == newSub.Name && s.MD5Hash.Hash == newSub.MD5Hash.Hash).ToList(); + foreach (SubmarineInfo existingSub in existingSubs) { existingSub.Dispose(); } - Submarine.AddToSavedSubs(newSub); + SubmarineInfo.AddToSavedSubs(newSub); for (int i = 0; i < 2; i++) { @@ -2045,8 +2088,8 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.SubList.Content.Children; var subElement = subListChildren.FirstOrDefault(c => - ((Submarine)c.UserData).Name == newSub.Name && - ((Submarine)c.UserData).MD5Hash.Hash == newSub.MD5Hash.Hash); + ((SubmarineInfo)c.UserData).Name == newSub.Name && + ((SubmarineInfo)c.UserData).MD5Hash.Hash == newSub.MD5Hash.Hash); if (subElement == null) continue; subElement.GetChild().TextColor = new Color(subElement.GetChild().TextColor, 1.0f); @@ -2074,17 +2117,17 @@ namespace Barotrauma.Networking if (campaign == null) { return; } GameMain.GameSession.SavePath = transfer.FilePath; - if (GameMain.GameSession.Submarine == null) + if (GameMain.GameSession.SubmarineInfo == null) { var gameSessionDoc = SaveUtil.LoadGameSessionDoc(GameMain.GameSession.SavePath); string subPath = Path.Combine(SaveUtil.TempPath, gameSessionDoc.Root.GetAttributeString("submarine", "")) + ".sub"; - GameMain.GameSession.Submarine = new Submarine(subPath, ""); + GameMain.GameSession.SubmarineInfo = new SubmarineInfo(subPath, ""); } SaveUtil.LoadGame(GameMain.GameSession.SavePath, GameMain.GameSession); - GameMain.GameSession?.Submarine?.CheckSubsLeftBehind(); - if (GameMain.GameSession?.Submarine?.Name != null) + GameMain.GameSession?.SubmarineInfo?.CheckSubsLeftBehind(); + if (GameMain.GameSession?.SubmarineInfo?.Name != null) { - GameMain.NetLobbyScreen.TryDisplayCampaignSubmarine(GameMain.GameSession.Submarine); + GameMain.NetLobbyScreen.TryDisplayCampaignSubmarine(GameMain.GameSession.SubmarineInfo); } campaign.LastSaveID = campaign.PendingSaveID; @@ -2119,8 +2162,7 @@ namespace Barotrauma.Networking { if (!permissions.HasFlag(ClientPermissions.ConsoleCommands)) { return false; } - commandName = commandName.ToLowerInvariant(); - if (permittedConsoleCommands.Any(c => c.ToLowerInvariant() == commandName)) { return true; } + if (permittedConsoleCommands.Any(c => c.Equals(commandName, StringComparison.OrdinalIgnoreCase))) { return true; } //check aliases foreach (DebugConsole.Command command in DebugConsole.Commands) @@ -2371,7 +2413,7 @@ namespace Barotrauma.Networking clientPeer.Send(msg, DeliveryMethod.Reliable); } - public void SetupNewCampaign(Submarine sub, string saveName, string mapSeed) + public void SetupNewCampaign(SubmarineInfo sub, string saveName, string mapSeed) { GameMain.NetLobbyScreen.CampaignSetupFrame.Visible = false; @@ -2537,6 +2579,11 @@ namespace Barotrauma.Networking inGameHUD.AddToGUIUpdateList(); GameMain.NetLobbyScreen.FileTransferFrame?.AddToGUIUpdateList(); } + + serverSettings.AddToGUIUpdateList(); + if (serverSettings.ServerLog.LogFrame != null) serverSettings.ServerLog.LogFrame.AddToGUIUpdateList(); + + GameMain.NetLobbyScreen?.PlayerFrame?.AddToGUIUpdateList(); } public void UpdateHUD(float deltaTime) @@ -2580,7 +2627,7 @@ namespace Barotrauma.Networking if (GUI.KeyboardDispatcher.Subscriber == null) { bool chatKeyHit = PlayerInput.KeyHit(InputType.Chat); - bool radioKeyHit = PlayerInput.KeyHit(InputType.RadioChat) && (Character.Controlled == null || Character.Controlled.SpeechImpediment < 0); + bool radioKeyHit = PlayerInput.KeyHit(InputType.RadioChat) && (Character.Controlled == null || Character.Controlled.SpeechImpediment < 100); if (chatKeyHit || radioKeyHit) { @@ -2626,8 +2673,6 @@ namespace Barotrauma.Networking } } } - serverSettings.AddToGUIUpdateList(); - if (serverSettings.ServerLog.LogFrame != null) serverSettings.ServerLog.LogFrame.AddToGUIUpdateList(); } public virtual void Draw(Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch) @@ -2740,73 +2785,106 @@ namespace Barotrauma.Networking }*/ } - public virtual bool SelectCrewCharacter(Character character, GUIComponent characterFrame) + public virtual bool SelectCrewCharacter(Character character, GUIComponent frame) { - if (character == null) { return false; } + if (character == null) return false; if (character != myCharacter) { var client = GameMain.NetworkMember.ConnectedClients.Find(c => c.Character == character); - if (client == null) { return false; } + if (client == null) return false; - var content = new GUIFrame(new RectTransform(new Vector2(0.9f, 1.0f - characterFrame.RectTransform.RelativeSize.Y), characterFrame.RectTransform, Anchor.BottomCenter, Pivot.TopCenter), - style: null); - - var mute = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform, Anchor.TopCenter), - TextManager.Get("Mute")) - { - Selected = client.MutedLocally, - OnSelected = (tickBox) => { client.MutedLocally = tickBox.Selected; return true; } - }; - - var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform, Anchor.BottomCenter), isHorizontal: true) - { - RelativeSpacing = 0.05f, - ChildAnchor = Anchor.CenterLeft, - Stretch = true - }; - - if (HasPermission(ClientPermissions.Ban)) - { - var banButton = new GUIButton(new RectTransform(new Vector2(0.45f, 0.9f), buttonContainer.RectTransform), - TextManager.Get("Ban"), style: "GUIButtonSmall") - { - UserData = client, - OnClicked = (btn, userdata) => { GameMain.NetLobbyScreen.BanPlayer(client); return false; } - }; - } - if (HasPermission(ClientPermissions.Kick)) - { - var kickButton = new GUIButton(new RectTransform(new Vector2(0.45f, 0.9f), buttonContainer.RectTransform), - TextManager.Get("Kick"), style: "GUIButtonSmall") - { - UserData = client, - OnClicked = (btn, userdata) => { GameMain.NetLobbyScreen.KickPlayer(client); return false; } - }; - } - else if (serverSettings.Voting.AllowVoteKick) - { - var kickVoteButton = new GUIButton(new RectTransform(new Vector2(0.45f, 0.9f), buttonContainer.RectTransform), - TextManager.Get("VoteToKick"), style: "GUIButtonSmall") - { - UserData = client, - OnClicked = (btn, userdata) => { VoteForKick(client); btn.Enabled = false; return true; } - }; - if (GameMain.NetworkMember.ConnectedClients != null) - { - kickVoteButton.Enabled = !client.HasKickVoteFromID(myID); - } - } + CreateSelectionRelatedButtons(client, frame); } return true; } + public virtual bool SelectCrewClient(Client client, GUIComponent frame) + { + if (client == null || client.ID == ID) return false; + CreateSelectionRelatedButtons(client, frame); + return true; + } + + private void CreateSelectionRelatedButtons(Client client, GUIComponent frame) + { + var content = new GUIFrame(new RectTransform(new Vector2(1f, 1.0f - frame.RectTransform.RelativeSize.Y), frame.RectTransform, Anchor.BottomCenter, Pivot.TopCenter), + style: null); + + var mute = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform, Anchor.TopCenter), + TextManager.Get("Mute")) + { + Selected = client.MutedLocally, + OnSelected = (tickBox) => { client.MutedLocally = tickBox.Selected; return true; } + }; + + var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.35f), content.RectTransform, Anchor.BottomCenter), isHorizontal: true, childAnchor: Anchor.BottomLeft) + { + RelativeSpacing = 0.05f, + Stretch = true + }; + + if (!GameMain.Client.GameStarted || (GameMain.Client.Character == null || GameMain.Client.Character.IsDead) && (client.Character == null || client.Character.IsDead)) + { + var messageButton = new GUIButton(new RectTransform(new Vector2(1f, 0.2f), content.RectTransform, Anchor.BottomCenter) { RelativeOffset = new Vector2(0f, buttonContainer.RectTransform.RelativeSize.Y) }, + TextManager.Get("message"), style: "GUIButtonSmall") + { + UserData = client, + OnClicked = (btn, userdata) => + { + chatBox.InputBox.Text = $"{client.Name}; "; + CoroutineManager.StartCoroutine(selectCoroutine()); + return false; + } + }; + } + + // Need a delayed selection due to the inputbox being deselected when a left click occurs outside of it + IEnumerable selectCoroutine() + { + yield return new WaitForSeconds(0.01f, true); + chatBox.InputBox.Select(chatBox.InputBox.Text.Length); + } + + if (HasPermission(ClientPermissions.Ban) && client.AllowKicking) + { + var banButton = new GUIButton(new RectTransform(new Vector2(0.45f, 0.9f), buttonContainer.RectTransform), + TextManager.Get("Ban"), style: "GUIButtonSmall") + { + UserData = client, + OnClicked = (btn, userdata) => { GameMain.NetLobbyScreen.BanPlayer(client); return false; } + }; + } + if (HasPermission(ClientPermissions.Kick) && client.AllowKicking) + { + var kickButton = new GUIButton(new RectTransform(new Vector2(0.45f, 0.9f), buttonContainer.RectTransform), + TextManager.Get("Kick"), style: "GUIButtonSmall") + { + UserData = client, + OnClicked = (btn, userdata) => { GameMain.NetLobbyScreen.KickPlayer(client); return false; } + }; + } + else if (serverSettings.Voting.AllowVoteKick && client.AllowKicking) + { + var kickVoteButton = new GUIButton(new RectTransform(new Vector2(0.45f, 0.9f), buttonContainer.RectTransform), + TextManager.Get("VoteToKick"), style: "GUIButtonSmall") + { + UserData = client, + OnClicked = (btn, userdata) => { VoteForKick(client); btn.Enabled = false; return true; } + }; + if (GameMain.NetworkMember.ConnectedClients != null) + { + kickVoteButton.Enabled = !client.HasKickVoteFromID(myID); + } + } + } + public void CreateKickReasonPrompt(string clientName, bool ban, bool rangeBan = false) { var banReasonPrompt = new GUIMessageBox( TextManager.Get(ban ? "BanReasonPrompt" : "KickReasonPrompt"), - "", new string[] { TextManager.Get("OK"), TextManager.Get("Cancel") }, new Vector2(0.25f, 0.2f), new Point(400, 200)); + "", new string[] { TextManager.Get("OK"), TextManager.Get("Cancel") }, new Vector2(0.25f, 0.22f), new Point(400, 220)); var content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.6f), banReasonPrompt.InnerFrame.RectTransform, Anchor.Center)); var banReasonBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.3f), content.RectTransform)) @@ -2820,14 +2898,16 @@ namespace Barotrauma.Networking if (ban) { - new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.15f), content.RectTransform), TextManager.Get("BanDuration")); - permaBanTickBox = new GUITickBox(new RectTransform(new Vector2(0.8f, 0.15f), content.RectTransform) { RelativeOffset = new Vector2(0.05f, 0.0f) }, - TextManager.Get("BanPermanent")) + + var labelContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.25f), content.RectTransform), isHorizontal: false); + new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), labelContainer.RectTransform), TextManager.Get("BanDuration")) { Padding = Vector4.Zero }; + var buttonContent = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), labelContainer.RectTransform), isHorizontal: true); + permaBanTickBox = new GUITickBox(new RectTransform(new Vector2(0.4f, 0.15f), buttonContent.RectTransform), TextManager.Get("BanPermanent")) { Selected = true }; - var durationContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.15f), content.RectTransform), isHorizontal: true) + var durationContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 1f), buttonContent.RectTransform), isHorizontal: true) { Visible = false }; @@ -2930,7 +3010,7 @@ namespace Barotrauma.Networking } if (GameMain.GameSession?.Submarine != null) { - errorLines.Add("Submarine: " + GameMain.GameSession.Submarine.Name); + errorLines.Add("Submarine: " + GameMain.GameSession.Submarine.Info.Name); } if (Level.Loaded != null) { @@ -2952,8 +3032,8 @@ namespace Barotrauma.Networking errorLines.Add(" " + DebugConsole.Messages[i].Time + " - " + DebugConsole.Messages[i].Text); } - string filePath = "event_error_log_client_" + Name + "_" + ToolBox.RemoveInvalidFileNameChars(DateTime.UtcNow.ToShortTimeString() + ".log"); - filePath = Path.Combine(ServerLog.SavePath, filePath); + string filePath = "event_error_log_client_" + Name + "_" + DateTime.UtcNow.ToShortTimeString() + ".log"; + filePath = Path.Combine(ServerLog.SavePath, ToolBox.RemoveInvalidFileNameChars(filePath)); if (!Directory.Exists(ServerLog.SavePath)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs index 4f1bc98e0..b279d2775 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/KarmaManager.cs @@ -47,11 +47,30 @@ namespace Barotrauma CreateLabeledSlider(parent, 0.0f, 1.0f, 0.01f, nameof(StructureDamageKarmaDecrease)); CreateLabeledSlider(parent, 0.0f, 1.0f, 0.01f, nameof(DamageFriendlyKarmaDecrease)); + //hide these for now if a localized text is not available + if (TextManager.ContainsTag("Karma." + nameof(StunFriendlyKarmaDecrease))) + { + CreateLabeledSlider(parent, 0.0f, 1.0f, 0.01f, nameof(StunFriendlyKarmaDecrease)); + } + if (TextManager.ContainsTag("Karma." + nameof(StunFriendlyKarmaDecreaseThreshold))) + { + CreateLabeledSlider(parent, 0.0f, 10.0f, 1.0f, nameof(StunFriendlyKarmaDecreaseThreshold)); + } CreateLabeledSlider(parent, 0.0f, 100.0f, 1.0f, nameof(ReactorMeltdownKarmaDecrease)); CreateLabeledSlider(parent, 0.0f, 10.0f, 0.05f, nameof(ReactorOverheatKarmaDecrease)); CreateLabeledNumberInput(parent, 0, 20, nameof(AllowedWireDisconnectionsPerMinute)); CreateLabeledSlider(parent, 0.0f, 20.0f, 0.5f, nameof(WireDisconnectionKarmaDecrease)); CreateLabeledSlider(parent, 0.0f, 30.0f, 1.0f, nameof(SpamFilterKarmaDecrease)); + + //hide these for now if a localized text is not available + if (TextManager.ContainsTag("Karma." + nameof(DangerousItemStealKarmaDecrease))) + { + CreateLabeledSlider(parent, 0.0f, 30.0f, 1.0f, nameof(DangerousItemStealKarmaDecrease)); + } + if (TextManager.ContainsTag("Karma." + nameof(DangerousItemStealBots))) + { + CreateLabeledTickBox(parent, nameof(DangerousItemStealBots)); + } } private void CreateLabeledSlider(GUIComponent parent, float min, float max, float step, string propertyName) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs index 41f8f65af..e104aa388 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/LidgrenClientPeer.cs @@ -39,7 +39,10 @@ namespace Barotrauma.Networking contentPackageOrderReceived = false; - netPeerConfiguration = new NetPeerConfiguration("barotrauma"); + netPeerConfiguration = new NetPeerConfiguration("barotrauma") + { + UseDualModeSockets = GameMain.Config.UseDualModeSockets + }; netPeerConfiguration.DisableMessageType(NetIncomingMessageType.DebugMessage | NetIncomingMessageType.WarningMessage | NetIncomingMessageType.Receipt | NetIncomingMessageType.ErrorMessage | NetIncomingMessageType.Error); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs index d8a94ef28..a2dd50d8f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2PClientPeer.cs @@ -137,8 +137,9 @@ namespace Barotrauma.Networking timeout -= deltaTime; heartbeatTimer -= deltaTime; - while (Steamworks.SteamNetworking.IsP2PPacketAvailable()) + for (int i=0;i<100;i++) { + if (!Steamworks.SteamNetworking.IsP2PPacketAvailable()) { break; } var packet = Steamworks.SteamNetworking.ReadP2PPacket(); if (packet.HasValue) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs index c334482f2..cf5534b9e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Primitives/Peers/SteamP2POwnerPeer.cs @@ -204,8 +204,9 @@ namespace Barotrauma.Networking } } - while (Steamworks.SteamNetworking.IsP2PPacketAvailable()) + for (int i=0;i<100;i++) { + if (!Steamworks.SteamNetworking.IsP2PPacketAvailable()) { break; } var packet = Steamworks.SteamNetworking.ReadP2PPacket(); if (packet.HasValue) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs index 229e0c7b1..93b64d6ae 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerLog.cs @@ -2,6 +2,8 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.Xna.Framework.Graphics; +using Barotrauma.Extensions; namespace Barotrauma.Networking { @@ -9,9 +11,19 @@ namespace Barotrauma.Networking { public GUIButton LogFrame; private GUIListBox listBox; + private GUIButton reverseButton; private string msgFilter; + private bool reverseOrder = false; + + private bool OnReverseClicked(GUIButton btn, object obj) + { + SetMessageReversal(!reverseOrder); + + return false; + } + public void CreateLogFrame() { LogFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas), style: "GUIBackgroundBlocker") @@ -80,7 +92,17 @@ namespace Barotrauma.Networking GUI.KeyboardDispatcher.Subscriber = searchBox; filterArea.RectTransform.MinSize = new Point(0, filterArea.RectTransform.Children.Max(c => c.MinSize.Y)); - listBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.95f), rightColumn.RectTransform)); + GUILayoutGroup listBoxLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.95f), rightColumn.RectTransform)) + { + Stretch = true, + RelativeSpacing = 0.0f + }; + + reverseButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), listBoxLayout.RectTransform), style: "UIToggleButtonVertical"); + reverseButton.Children.ForEach(c => c.SpriteEffects = reverseOrder ? SpriteEffects.FlipVertically : SpriteEffects.None); + reverseButton.OnClicked = OnReverseClicked; + + listBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.95f), listBoxLayout.RectTransform)); GUIButton closeButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.05f), rightColumn.RectTransform), TextManager.Get("Close")) { @@ -107,7 +129,7 @@ namespace Barotrauma.Networking msgFilter = ""; } - public void AssignLogFrame(GUIListBox inListBox, GUIComponent tickBoxContainer, GUITextBox searchBox) + public void AssignLogFrame(GUIButton inReverseButton, GUIListBox inListBox, GUIComponent tickBoxContainer, GUITextBox searchBox) { searchBox.OnTextChanged += (textBox, text) => { @@ -144,6 +166,10 @@ namespace Barotrauma.Networking inListBox.ClearChildren(); listBox = inListBox; + reverseButton = inReverseButton; + reverseButton.Children.ForEach(c => c.SpriteEffects = reverseOrder ? SpriteEffects.FlipVertically : SpriteEffects.None); + reverseButton.OnClicked = OnReverseClicked; + var currLines = lines.ToList(); foreach (LogMessage line in currLines) { @@ -158,8 +184,34 @@ namespace Barotrauma.Networking { float prevSize = listBox.BarSize; - var textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), listBox.Content.RectTransform), - line.Text, wrap: true, font: GUI.SmallFont) + GUIFrame textContainer = null; + + Anchor anchor = Anchor.TopLeft; + Pivot pivot = Pivot.TopLeft; + if (line.RichData != null) + { + foreach (var data in line.RichData) + { + UInt64 id = 0; + if (!UInt64.TryParse(data.Metadata, out id)) { return; } + Client client = GameMain.Client.ConnectedClients.Find(c => c.SteamID == id); + client ??= GameMain.Client.ConnectedClients.Find(c => c.ID == id); + if (client != null && client.Karma < 40.0f) + { + textContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), listBox.Content.RectTransform), + style: null, color: new Color(0xff111155)) + { + CanBeFocused = false + }; + anchor = Anchor.CenterLeft; + pivot = Pivot.CenterLeft; + break; + } + } + } + + var textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), (textContainer ?? listBox.Content).RectTransform, anchor, pivot), + line.RichData, line.SanitizedText, wrap: true, font: GUI.SmallFont) { TextColor = messageColor[line.Type], Visible = !msgTypeHidden[(int)line.Type], @@ -167,6 +219,38 @@ namespace Barotrauma.Networking UserData = line }; + if (textContainer != null) + { + textContainer.RectTransform.NonScaledSize = new Point(textContainer.RectTransform.NonScaledSize.X, textBlock.RectTransform.NonScaledSize.Y + 5); + textBlock.SetTextPos(); + textBlock.RectTransform.Resize(textContainer.RectTransform.NonScaledSize); + } + + if (reverseOrder) + { + textBlock.RectTransform.SetAsFirstChild(); + } + + if (line.RichData != null) + { + foreach (var data in line.RichData) + { + textBlock.ClickableAreas.Add(new GUITextBlock.ClickableArea() + { + Data = data, + OnClick = (component, area) => + { + UInt64 id = 0; + if (!UInt64.TryParse(area.Data.Metadata, out id)) { return; } + Client client = GameMain.Client.ConnectedClients.Find(c => c.SteamID == id); + client ??= GameMain.Client.ConnectedClients.Find(c => c.ID == id); + if (client == null) { return; } + GameMain.NetLobbyScreen.SelectPlayer(client); + } + }); + } + } + if ((prevSize == 1.0f && listBox.BarScroll == 0.0f) || (prevSize < 1.0f && listBox.BarScroll == 1.0f)) listBox.BarScroll = 1.0f; } @@ -195,6 +279,16 @@ namespace Barotrauma.Networking return true; } + private void SetMessageReversal(bool reverse) + { + if (reverseOrder == reverse) { return; } + + reverseOrder = reverse; + reverseButton.Children.ForEach(c => c.SpriteEffects = reverseOrder ? SpriteEffects.FlipVertically : SpriteEffects.None); + + listBox.Content.RectTransform.ReverseChildren(); + } + public bool ClearFilter(GUIComponent button, object obj) { var searchBox = button.UserData as GUITextBox; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index a279517b2..c299c8380 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -571,6 +571,9 @@ namespace Barotrauma.Networking var ragdollButtonBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), TextManager.Get("ServerSettingsAllowRagdollButton")); GetPropertyData("AllowRagdollButton").AssignGUIComponent(ragdollButtonBox); + var disableBotConversationsBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), TextManager.Get("ServerSettingsDisableBotConversations")); + GetPropertyData("DisableBotConversations").AssignGUIComponent(disableBotConversationsBox); + /*var traitorRatioBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), roundsTab.RectTransform), TextManager.Get("ServerSettingsUseTraitorRatio")); CreateLabeledSlider(roundsTab, "", out slider, out sliderLabel); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs index 2979156d5..cc77ca939 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/SteamManager.cs @@ -10,11 +10,14 @@ using RestSharp.Contrib; using System.Xml.Linq; using System.Xml; using Color = Microsoft.Xna.Framework.Color; +using System.Runtime.InteropServices; namespace Barotrauma.Steam { static partial class SteamManager { + private static Dictionary modCopiesInProgress = new Dictionary(); + private static void InitializeProjectSpecific() { if (isInitialized) { return; } @@ -35,6 +38,11 @@ namespace Barotrauma.Steam popularTags.Insert(i, commonness.Key); i++; } + + LogSteamworksNetworkingDelegate = LogSteamworksNetworking; + + IntPtr logSteamworksNetworkingPtr = Marshal.GetFunctionPointerForDelegate(LogSteamworksNetworkingDelegate); + Steamworks.SteamNetworkingUtils.SetDebugOutputFunction(Steamworks.Data.DebugOutputType.Everything, logSteamworksNetworkingPtr); } } catch (DllNotFoundException) @@ -61,22 +69,17 @@ namespace Barotrauma.Steam } } + public static bool NetworkingDebugLog = false; + + private static Steamworks.Data.FSteamNetworkingSocketsDebugOutput LogSteamworksNetworkingDelegate; + + private static void LogSteamworksNetworking(Steamworks.Data.DebugOutputType nType, string pszMsg) + { + if (NetworkingDebugLog) { DebugConsole.NewMessage($"({nType}) {pszMsg}", Color.Orange); } + } + private static void UpdateProjectSpecific(float deltaTime) { - for (int i=0;i<(ugcResultPageTasks?.Count ?? 0);i++) - { - var task = ugcResultPageTasks[i]; - if (task.IsCompleted) - { - if (!task.IsCompletedSuccessfully) - { - DebugConsole.ThrowError("Failed to retrieve Steam Workshop page info: TaskStatus = "+task.Status.ToString()); - } - ugcResultPageTasks.RemoveAt(i); - i--; - } - } - if (ugcSubscriptionTasks != null) { var ugcSubscriptionKeys = ugcSubscriptionTasks.Keys.ToList(); @@ -525,7 +528,37 @@ namespace Barotrauma.Steam } } - private static List ugcResultPageTasks; + private static async Task> GetWorkshopItemsAsync(Steamworks.Ugc.Query query, int clampResults = 0, Predicate itemPredicate=null) + { + await Task.Yield(); + + int pageIndex = 1; + Steamworks.Ugc.ResultPage? resultPage = await query.GetPageAsync(pageIndex); + + List retVal = new List(); + while (resultPage.HasValue && resultPage?.ResultCount > 0) + { + if (itemPredicate != null) + { + retVal.AddRange(resultPage.Value.Entries.Where(it => itemPredicate(it))); + } + else + { + retVal.AddRange(resultPage.Value.Entries); + } + + if (clampResults > 0 && retVal.Count >= clampResults) + { + retVal = retVal.Take(clampResults).ToList(); + break; + } + + pageIndex++; + resultPage = await query.GetPageAsync(pageIndex); + } + + return retVal; + } public static void GetSubscribedWorkshopItems(Action> onItemsFound, List requireTags = null) { @@ -535,30 +568,9 @@ namespace Barotrauma.Steam .RankedByTotalUniqueSubscriptions() .WhereUserSubscribed() .WithLongDescription(); - if (requireTags != null) query.WithTags(requireTags); + if (requireTags != null) { query = query.WithTags(requireTags); } - ugcResultPageTasks ??= new List(); - ugcResultPageTasks.Add(Task.Run(async () => - { - int processedResults = 0; int pageIndex = 1; - Steamworks.Ugc.ResultPage? resultPage = await query.GetPageAsync(pageIndex); - - while (resultPage.HasValue && resultPage?.ResultCount > 0) - { - onItemsFound?.Invoke(resultPage.Value.Entries.ToList()); - - processedResults += resultPage.Value.ResultCount; - pageIndex++; - if (processedResults < resultPage?.TotalCount) - { - resultPage = await query.GetPageAsync(pageIndex); - } - else - { - resultPage = null; - } - } - })); + TaskPool.Add(GetWorkshopItemsAsync(query), (task) => { onItemsFound?.Invoke(task.Result); }); } public static void GetPopularWorkshopItems(Action> onItemsFound, int amount, List requireTags = null) @@ -570,65 +582,40 @@ namespace Barotrauma.Steam .WithLongDescription(); if (requireTags != null) query.WithTags(requireTags); - ugcResultPageTasks ??= new List(); - ugcResultPageTasks.Add(Task.Run(async () => - { - int processedResults = 0; int pageIndex = 1; - Steamworks.Ugc.ResultPage? resultPage = await query.GetPageAsync(pageIndex); + TaskPool.Add(GetWorkshopItemsAsync(query, amount, (item) => !item.IsSubscribed), (task) => { + var entries = task.Result; - while (resultPage.HasValue && resultPage?.ResultCount > 0) + //count the number of each unique tag + foreach (var item in entries) { - var entries = resultPage.Value.Entries.ToList(); - //count the number of each unique tag - foreach (var item in entries) + foreach (string tag in item.Tags) { - foreach (string tag in item.Tags) + if (string.IsNullOrEmpty(tag)) { continue; } + string caseInvariantTag = tag.ToLowerInvariant(); + if (!tagCommonness.ContainsKey(caseInvariantTag)) { - if (string.IsNullOrEmpty(tag)) { continue; } - string caseInvariantTag = tag.ToLowerInvariant(); - if (!tagCommonness.ContainsKey(caseInvariantTag)) - { - tagCommonness[caseInvariantTag] = 1; - } - else - { - tagCommonness[caseInvariantTag]++; - } + tagCommonness[caseInvariantTag] = 1; } - } - //populate the popularTags list with tags sorted by commonness - popularTags.Clear(); - foreach (KeyValuePair tagCommonnessKVP in tagCommonness) - { - int i = 0; - while (i < popularTags.Count && - tagCommonness[popularTags[i]] > tagCommonnessKVP.Value) + else { - i++; + tagCommonness[caseInvariantTag]++; } - popularTags.Insert(i, tagCommonnessKVP.Key); - } - - var nonSubscribedItems = entries.Where(it => !it.IsSubscribed); - if (nonSubscribedItems.Count() > (amount-processedResults)) - { - nonSubscribedItems = nonSubscribedItems.Take(amount - processedResults); - } - - onItemsFound?.Invoke(nonSubscribedItems.ToList()); - - processedResults += resultPage.Value.ResultCount; - pageIndex++; - if (processedResults < resultPage?.TotalCount && processedResults < amount) - { - resultPage = await query.GetPageAsync(pageIndex); - } - else - { - resultPage = null; } } - })); + //populate the popularTags list with tags sorted by commonness + popularTags.Clear(); + foreach (KeyValuePair tagCommonnessKVP in tagCommonness) + { + int i = 0; + while (i < popularTags.Count && + tagCommonness[popularTags[i]] > tagCommonnessKVP.Value) + { + i++; + } + popularTags.Insert(i, tagCommonnessKVP.Key); + } + onItemsFound?.Invoke(task.Result); + }); } public static void GetPublishedWorkshopItems(Action> onItemsFound, List requireTags = null) @@ -641,28 +628,7 @@ namespace Barotrauma.Steam .WithLongDescription(); if (requireTags != null) query.WithTags(requireTags); - ugcResultPageTasks ??= new List(); - ugcResultPageTasks.Add(Task.Run(async () => - { - int processedResults = 0; int pageIndex = 1; - Steamworks.Ugc.ResultPage? resultPage = await query.GetPageAsync(pageIndex); - - while (resultPage.HasValue && resultPage?.ResultCount > 0) - { - onItemsFound?.Invoke(resultPage.Value.Entries.ToList()); - - processedResults += resultPage.Value.ResultCount; - pageIndex++; - if (processedResults < resultPage?.TotalCount) - { - resultPage = await query.GetPageAsync(pageIndex); - } - else - { - resultPage = null; - } - } - })); + TaskPool.Add(GetWorkshopItemsAsync(query), (task) => { onItemsFound?.Invoke(task.Result); }); } private static Dictionary ugcSubscriptionTasks; @@ -926,7 +892,7 @@ namespace Barotrauma.Steam /// /// Enables a workshop item by moving it to the game folder. /// - public static bool EnableWorkShopItem(Steamworks.Ugc.Item? item, bool allowFileOverwrite, out string errorMsg) + public static bool EnableWorkShopItem(Steamworks.Ugc.Item? item, bool allowFileOverwrite, out string errorMsg, bool selectContentPackage = false, bool suppressInstallNotif = false) { if (!(item?.IsInstalled ?? false)) { @@ -952,10 +918,18 @@ namespace Barotrauma.Steam if (ContentPackage.List.Any(cp => cp.Path.CleanUpPath() == newContentPackagePath.CleanUpPath())) { - errorMsg = TextManager.GetWithVariables("WorkshopErrorSamePathInstalled", - new string[] { "[itemname]", "[itempath]" }, - new string[] { item?.Title, Path.GetDirectoryName(newContentPackagePath) }); - return false; + if (item?.Owner.Id != Steamworks.SteamClient.SteamId) + { + errorMsg = TextManager.GetWithVariables("WorkshopErrorSamePathInstalled", + new string[] { "[itemname]", "[itempath]" }, + new string[] { item?.Title, Path.GetDirectoryName(newContentPackagePath) }); + return false; + } + else + { + RemoveMods(cp => !string.IsNullOrWhiteSpace(cp.SteamWorkshopUrl) && cp.SteamWorkshopUrl == contentPackage.SteamWorkshopUrl, + false); + } } if (!contentPackage.IsCompatible()) @@ -972,57 +946,116 @@ namespace Barotrauma.Steam return false; } - GameMain.Config.SuppressModFolderWatcher = true; + Task newTask = null; - CopyWorkShopItem(item, contentPackage, newContentPackagePath, metaDataFilePath, allowFileOverwrite, out errorMsg); - - var newPackage = new ContentPackage(contentPackage.Path, newContentPackagePath) + lock (modCopiesInProgress) { - SteamWorkshopUrl = item?.Url, - InstallTime = item?.Updated > item?.Created ? item?.Updated : item?.Created - }; - - foreach (ContentFile contentFile in newPackage.Files) - { - contentFile.Path = CorrectContentFilePath(contentFile.Path, contentPackage, true); + if (modCopiesInProgress.ContainsKey(item.Value.Id)) + { + if (!modCopiesInProgress[item.Value.Id].IsCompleted && + !modCopiesInProgress[item.Value.Id].IsFaulted && + !modCopiesInProgress[item.Value.Id].IsCanceled) + { + errorMsg = ""; return true; + } + modCopiesInProgress.Remove(item.Value.Id); + } + newTask = CopyWorkShopItemAsync(item, contentPackage, newContentPackagePath, metaDataFilePath, allowFileOverwrite); + modCopiesInProgress.Add(item.Value.Id, newTask); } + + TaskPool.Add(newTask, + contentPackage, + (task, cp) => + { + if (task.IsFaulted || task.IsCanceled) + { + DebugConsole.ThrowError($"Failed to copy \"{item?.Title}\"", task.Exception); + GameMain.SteamWorkshopScreen?.SetReinstallButtonStatus(item, true, GUI.Style.Red); + return; + } + if (!string.IsNullOrWhiteSpace(task.Result)) + { + DebugConsole.ThrowError($"Failed to copy \"{item?.Title}\": {task.Result}"); + GameMain.SteamWorkshopScreen?.SetReinstallButtonStatus(item, true, GUI.Style.Red); + return; + } - if (!Directory.Exists(Path.GetDirectoryName(newContentPackagePath))) - { - Directory.CreateDirectory(Path.GetDirectoryName(newContentPackagePath)); - } - newPackage.Save(newContentPackagePath); - ContentPackage.List.Add(newPackage); - if (newPackage.CorePackage) - { - GameMain.Config.SelectCorePackage(newPackage); - } - else - { - GameMain.Config.SelectContentPackage(newPackage); - } - GameMain.Config.SaveNewPlayerConfig(); + GameMain.Config.SuppressModFolderWatcher = true; - GameMain.Config.WarnIfContentPackageSelectionDirty(); + var newPackage = new ContentPackage(cp.Path, newContentPackagePath) + { + SteamWorkshopUrl = item?.Url, + InstallTime = item?.Updated > item?.Created ? item?.Updated : item?.Created + }; - GameMain.Config.SuppressModFolderWatcher = false; + foreach (ContentFile contentFile in newPackage.Files) + { + contentFile.Path = CorrectContentFilePath(contentFile.Path, cp, true); + } - if (newPackage.Files.Any(f => f.Type == ContentType.Submarine)) - { - Submarine.RefreshSavedSubs(); - } + if (!Directory.Exists(Path.GetDirectoryName(newContentPackagePath))) + { + Directory.CreateDirectory(Path.GetDirectoryName(newContentPackagePath)); + } + newPackage.Save(newContentPackagePath); + ContentPackage.List.Add(newPackage); + if (selectContentPackage) + { + if (newPackage.CorePackage) + { + GameMain.Config.SelectCorePackage(newPackage); + } + else + { + GameMain.Config.SelectContentPackage(newPackage); + } + GameMain.Config.SaveNewPlayerConfig(); + + GameMain.Config.WarnIfContentPackageSelectionDirty(); + + if (newPackage.Files.Any(f => f.Type == ContentType.Submarine)) + { + SubmarineInfo.RefreshSavedSubs(); + } + } + else if (!suppressInstallNotif) + { + GameMain.MainMenuScreen?.SetEnableModsNotification(true); + } + + GameMain.Config.SuppressModFolderWatcher = false; + + GameMain.SteamWorkshopScreen?.SetReinstallButtonStatus(item, true, GUI.Style.Green); + + }); + errorMsg = ""; return true; } - private static bool CopyWorkShopItem(Steamworks.Ugc.Item? item, ContentPackage contentPackage, string newContentPackagePath, string metaDataFilePath, bool allowFileOverwrite, out string errorMsg) + /// + /// Asynchronously copies a Workshop item into the Mods folder. + /// + /// Returns an empty string on success, otherwise returns an error message. + private async static Task CopyWorkShopItemAsync(Steamworks.Ugc.Item? item, ContentPackage contentPackage, string newContentPackagePath, string metaDataFilePath, bool allowFileOverwrite) { - errorMsg = ""; + await Task.Yield(); + + string targetPath = Path.GetDirectoryName(GetWorkshopItemContentPackagePath(contentPackage)); + string copyingPath = Path.Combine(targetPath, CopyIndicatorFileName); + + string errorMsg = ""; if (contentPackage.GameVersion > new Version(0, 9, 1, 0)) { - SaveUtil.CopyFolder(item?.Directory, Path.GetDirectoryName(GetWorkshopItemContentPackagePath(contentPackage)), copySubDirs: true, overwriteExisting: true); - return true; + Directory.CreateDirectory(targetPath); + File.WriteAllText(copyingPath, "TEMPORARY FILE"); + + SaveUtil.CopyFolder(item?.Directory, targetPath, copySubDirs: true, overwriteExisting: true); + + File.Delete(copyingPath); + return ""; } var allPackageFiles = Directory.GetFiles(item?.Directory, "*", SearchOption.AllDirectories); @@ -1042,7 +1075,7 @@ namespace Barotrauma.Steam { errorMsg = TextManager.GetWithVariables("WorkshopErrorOverwriteOnEnable", new string[2] { "[itemname]", "[filename]" }, new string[2] { item?.Title, newContentPackagePath }); DebugConsole.NewMessage(errorMsg, Color.Red); - return false; + return errorMsg; } foreach (ContentFile contentFile in contentPackage.Files) @@ -1053,84 +1086,79 @@ namespace Barotrauma.Steam { errorMsg = TextManager.GetWithVariables("WorkshopErrorOverwriteOnEnable", new string[2] { "[itemname]", "[filename]" }, new string[2] { item?.Title, contentFile.Path }); DebugConsole.NewMessage(errorMsg, Color.Red); - return false; + return errorMsg; } } } - try + Directory.CreateDirectory(targetPath); + File.WriteAllText(copyingPath, "TEMPORARY FILE"); + + foreach (ContentFile contentFile in contentPackage.Files) { - foreach (ContentFile contentFile in contentPackage.Files) + contentFile.Path = contentFile.Path.CleanUpPath(); + string sourceFile = Path.Combine(item?.Directory, contentFile.Path); + if (!File.Exists(sourceFile)) { - contentFile.Path = contentFile.Path.CleanUpPath(); - string sourceFile = Path.Combine(item?.Directory, contentFile.Path); - if (!File.Exists(sourceFile)) + string[] splitPath = contentFile.Path.Split('/'); + if (splitPath.Length >= 2 && splitPath[0] == "Mods") { - string[] splitPath = contentFile.Path.Split('/'); - if (splitPath.Length >= 2 && splitPath[0] == "Mods") - { - sourceFile = Path.Combine(item?.Directory, string.Join("/", splitPath.Skip(2))); - } + sourceFile = Path.Combine(item?.Directory, string.Join("/", splitPath.Skip(2))); } + } - contentFile.Path = CorrectContentFilePath(contentFile.Path, contentPackage, false); + contentFile.Path = CorrectContentFilePath(contentFile.Path, contentPackage, + contentFile.Type != ContentType.Submarine); - - //path not allowed -> the content file must be a reference to an external file (such as some vanilla file outside the Mods folder) - if (!ContentPackage.IsModFilePathAllowed(contentFile)) + //path not allowed -> the content file must be a reference to an external file (such as some vanilla file outside the Mods folder) + if (!ContentPackage.IsModFilePathAllowed(contentFile)) + { + //the content package is trying to copy a file to a prohibited path, which is not allowed + if (File.Exists(sourceFile)) { - //the content package is trying to copy a file to a prohibited path, which is not allowed - if (File.Exists(sourceFile)) - { - errorMsg = TextManager.GetWithVariable("WorkshopErrorIllegalPathOnEnable", "[filename]", contentFile.Path); - return false; - } - //not trying to copy anything, so this is a reference to an external file - //if the external file doesn't exist, we cannot enable the package - else if (!File.Exists(contentFile.Path)) - { - errorMsg = TextManager.GetWithVariable("WorkshopErrorEnableFailed", "[itemname]", item?.Title) + " " + TextManager.GetWithVariable("WorkshopFileNotFound", "[path]", "\"" + contentFile.Path + "\""); - return false; - } + errorMsg = TextManager.GetWithVariable("WorkshopErrorIllegalPathOnEnable", "[filename]", contentFile.Path); + return errorMsg; + } + //not trying to copy anything, so this is a reference to an external file + //if the external file doesn't exist, we cannot enable the package + else if (!File.Exists(contentFile.Path)) + { + errorMsg = TextManager.GetWithVariable("WorkshopErrorEnableFailed", "[itemname]", item?.Title) + " " + TextManager.GetWithVariable("WorkshopFileNotFound", "[path]", "\"" + contentFile.Path + "\""); + return errorMsg; + } + continue; + } + else if (!File.Exists(sourceFile)) + { + if (File.Exists(contentFile.Path)) + { + //the file is already present in the game folder, all good continue; } - else if (!File.Exists(sourceFile)) + else { - if (File.Exists(contentFile.Path)) - { - //the file is already present in the game folder, all good - continue; - } - else - { - //file not present in either the mod or the game folder -> cannot enable the package - errorMsg = TextManager.GetWithVariable("WorkshopErrorEnableFailed", "[itemname]", item?.Title) + " " + TextManager.GetWithVariable("WorkshopFileNotFound", "[path]", "\"" + contentFile.Path + "\""); - return false; - } + //file not present in either the mod or the game folder -> cannot enable the package + errorMsg = TextManager.GetWithVariable("WorkshopErrorEnableFailed", "[itemname]", item?.Title) + " " + TextManager.GetWithVariable("WorkshopFileNotFound", "[path]", "\"" + contentFile.Path + "\""); + return errorMsg; } - - //make sure the destination directory exists - Directory.CreateDirectory(Path.GetDirectoryName(contentFile.Path)); - CorrectContentFileCopy(contentPackage, sourceFile, contentFile.Path, overwrite: true); } - foreach (string nonContentFile in nonContentFiles) - { - string sourceFile = Path.Combine(item?.Directory, nonContentFile); - if (!File.Exists(sourceFile)) { continue; } - string destinationPath = CorrectContentFilePath(nonContentFile, contentPackage, false); - Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)); - CorrectContentFileCopy(contentPackage, sourceFile, destinationPath, overwrite: true); - } + //make sure the destination directory exists + Directory.CreateDirectory(Path.GetDirectoryName(contentFile.Path)); + CorrectContentFileCopy(contentPackage, sourceFile, contentFile.Path, overwrite: true); } - catch (Exception e) + + foreach (string nonContentFile in nonContentFiles) { - errorMsg = TextManager.GetWithVariable("WorkshopErrorEnableFailed", "[itemname]", item?.Title) + " {" + e.Message + "}"; - DebugConsole.NewMessage(errorMsg, Color.Red); - return false; + string sourceFile = Path.Combine(item?.Directory, nonContentFile); + if (!File.Exists(sourceFile)) { continue; } + string destinationPath = CorrectContentFilePath(nonContentFile, contentPackage, false); + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)); + CorrectContentFileCopy(contentPackage, sourceFile, destinationPath, overwrite: true); } - return true; + File.Delete(copyingPath); + return ""; } private static bool CheckFileEquality(string filePath1, string filePath2) @@ -1149,6 +1177,47 @@ namespace Barotrauma.Steam } } + private static void RemoveMods(Func predicate, bool delete = true) + { + var toRemove = ContentPackage.List.Where(predicate).ToList(); + var packagesToDeselect = GameMain.Config.SelectedContentPackages.Where(p => toRemove.Contains(p)).ToList(); + foreach (var cp in packagesToDeselect) + { + if (cp.CorePackage) + { + GameMain.Config.SelectCorePackage(ContentPackage.List.Find(cpp => cpp.CorePackage && !toRemove.Contains(cpp))); + } + else + { + GameMain.Config.DeselectContentPackage(cp); + } + } + + if (delete) + { + foreach (var cp in toRemove) + { + try + { + string path = Path.GetDirectoryName(cp.Path); + if (Directory.Exists(path)) { Directory.Delete(path, true); } + } + catch (Exception e) + { + DebugConsole.ThrowError($"An error occurred while attempting to delete {Path.GetDirectoryName(cp.Path)}", e); + } + } + } + + ContentPackage.List.RemoveAll(cp => toRemove.Contains(cp)); + GameMain.Config.SelectedContentPackages.RemoveAll(cp => !ContentPackage.List.Contains(cp)); + + ContentPackage.SortContentPackages(); + GameMain.Config.SaveNewPlayerConfig(); + + GameMain.Config.WarnIfContentPackageSelectionDirty(); + } + /// /// Disables a workshop item by removing the files from the game folder. /// @@ -1173,44 +1242,11 @@ namespace Barotrauma.Steam GameMain.Config.SuppressModFolderWatcher = true; try { - - var toRemove = ContentPackage.List.Where(cp => !string.IsNullOrWhiteSpace(cp.SteamWorkshopUrl) && cp.SteamWorkshopUrl == contentPackage.SteamWorkshopUrl).ToList(); - var packagesToDeselect = GameMain.Config.SelectedContentPackages.Where(p => toRemove.Contains(p)).ToList(); - foreach (var cp in packagesToDeselect) - { - if (cp.CorePackage) - { - GameMain.Config.SelectCorePackage(ContentPackage.List.Find(cpp => cpp.CorePackage && !toRemove.Contains(cpp))); - } - else - { - GameMain.Config.DeselectContentPackage(cp); - } - } - - foreach (var cp in toRemove) - { - try - { - Directory.Delete(Path.GetDirectoryName(cp.Path), true); - } - catch (Exception e) - { - DebugConsole.ThrowError($"An error occurred while attempting to delete {Path.GetDirectoryName(cp.Path)}", e); - } - } - - ContentPackage.List.RemoveAll(cp => toRemove.Contains(cp)); - GameMain.Config.SelectedContentPackages.RemoveAll(cp => !ContentPackage.List.Contains(cp)); - - ContentPackage.SortContentPackages(); - GameMain.Config.SaveNewPlayerConfig(); - - GameMain.Config.WarnIfContentPackageSelectionDirty(); + RemoveMods(cp => !string.IsNullOrWhiteSpace(cp.SteamWorkshopUrl) && cp.SteamWorkshopUrl == contentPackage.SteamWorkshopUrl); } catch (Exception e) { - errorMsg = "Disabling the workshop item \"" + item?.Title + "\" failed. " + e.Message; + errorMsg = "Disabling the workshop item \"" + item?.Title + "\" failed. " + e.Message + "\n" + e.StackTrace; if (!noLog) { DebugConsole.NewMessage(errorMsg, Microsoft.Xna.Framework.Color.Red); @@ -1219,6 +1255,8 @@ namespace Barotrauma.Steam } GameMain.Config.SuppressModFolderWatcher = false; + GameMain.SteamWorkshopScreen?.SetReinstallButtonStatus(item, false, null); + errorMsg = ""; return true; } @@ -1314,7 +1352,7 @@ namespace Barotrauma.Steam return upToDate; } - public static async Task AutoUpdateWorkshopItems() + public static async Task AutoUpdateWorkshopItemsAsync() { if (!isInitialized) { return false; } @@ -1322,60 +1360,104 @@ namespace Barotrauma.Steam .WhereUserSubscribed() .WithLongDescription(); - DateTime startTime = DateTime.Now; - DateTime endTime = startTime + new TimeSpan(0, 0, 30); + List items = await GetWorkshopItemsAsync(query); - int processedResults = 0; int pageIndex = 1; - Steamworks.Ugc.ResultPage? resultPage = await query.GetPageAsync(pageIndex); + GameMain.Config.SuppressModFolderWatcher = true; - while (resultPage.HasValue && resultPage?.ResultCount > 0) + //remove mods that the player is no longer subscribed to + RemoveMods(cp => !string.IsNullOrWhiteSpace(cp.SteamWorkshopUrl) && !items.Any(it => it.Id == GetWorkshopItemIDFromUrl(cp.SteamWorkshopUrl))); + + GameMain.Config.SuppressModFolderWatcher = false; + + + List updateNotifications = new List(); + foreach (var item in items) { - if (DateTime.Now > endTime) + try { - return false; - } - foreach (var item in resultPage.Value.Entries) - { - try + if (!item.IsInstalled) { continue; } + + bool installedSuccessfully = false; + string errorMsg; + if (!CheckWorkshopItemEnabled(item)) { - if (!item.IsInstalled || !CheckWorkshopItemEnabled(item) || CheckWorkshopItemUpToDate(item)) { continue; } - if (!UpdateWorkshopItem(item, out string errorMsg)) + installedSuccessfully = EnableWorkShopItem(item, true, out errorMsg); + } + else if (!CheckWorkshopItemUpToDate(item)) + { + installedSuccessfully = UpdateWorkshopItem(item, out errorMsg); + } + else + { + continue; + } + + if (!installedSuccessfully) + { + CrossThread.RequestExecutionOnMainThread(() => + { + DebugConsole.NewMessage(errorMsg, Color.Red); + string errorId = errorMsg; + if (!GUIMessageBox.MessageBoxes.Any(m => m.UserData as string == errorId)) + { + new GUIMessageBox( + TextManager.Get("Error"), + TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { item.Title, errorMsg })) + { + UserData = errorId + }; + } + }); + } + else + { + updateNotifications.Add(TextManager.GetWithVariable("WorkshopItemUpdated", "[itemname]", item.Title)); + } + } + catch (Exception e) + { + CrossThread.RequestExecutionOnMainThread(() => + { + string errorId = e.Message; + if (!GUIMessageBox.MessageBoxes.Any(m => m.UserData as string == errorId)) { - DebugConsole.ThrowError(errorMsg); new GUIMessageBox( TextManager.Get("Error"), - TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { item.Title, errorMsg })); + TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { item.Title, e.Message + ", " + e.TargetSite })) + { + UserData = errorId + }; } - else - { - //TODO: potential race condition - new GUIMessageBox("", TextManager.GetWithVariable("WorkshopItemUpdated", "[itemname]", item.Title)); - } - } - catch (Exception e) - { - new GUIMessageBox( - TextManager.Get("Error"), - TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { item.Title, e.Message + ", " + e.TargetSite })); GameAnalyticsManager.AddErrorEventOnce( "SteamManager.AutoUpdateWorkshopItems:" + e.Message, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, "Failed to autoupdate workshop item \"" + item.Title + "\". " + e.Message + "\n" + e.StackTrace); - } - } - - processedResults += resultPage.Value.ResultCount; - pageIndex++; - if (processedResults < resultPage?.TotalCount) - { - resultPage = await query.GetPageAsync(pageIndex); - } - else - { - resultPage = null; + }); } } + if (updateNotifications.Count > 0) + { + CrossThread.RequestExecutionOnMainThread(() => + { + while (updateNotifications.Count > 0) + { + int notificationsPerMsgBox = 20; + new GUIMessageBox("", string.Join('\n', updateNotifications.Take(notificationsPerMsgBox)), + relativeSize: new Microsoft.Xna.Framework.Vector2(0.5f, 0.0f), + minSize: new Microsoft.Xna.Framework.Point(600, 0)); + updateNotifications.RemoveRange(0, Math.Min(notificationsPerMsgBox, updateNotifications.Count)); + } + }); + } + + List tasks; + lock (modCopiesInProgress) + { + tasks = modCopiesInProgress.Values.ToList(); + } + await Task.WhenAll(tasks); + return true; } @@ -1383,7 +1465,10 @@ namespace Barotrauma.Steam { errorMsg = ""; if (!(item?.IsInstalled ?? false)) { return false; } - if (!DisableWorkShopItem(item, false, out errorMsg)) { return false; } + if (item?.Owner.Id != Steamworks.SteamClient.SteamId) + { + if (!DisableWorkShopItem(item, false, out errorMsg)) { return false; } + } if (!EnableWorkShopItem(item, allowFileOverwrite: false, errorMsg: out errorMsg)) { return false; } return true; @@ -1391,8 +1476,9 @@ namespace Barotrauma.Steam private static string GetWorkshopItemContentPackagePath(ContentPackage contentPackage) { - string packageName = contentPackage.Name; + string packageName = contentPackage.Name.Trim(); packageName = ToolBox.RemoveInvalidFileNameChars(packageName); + while (packageName.Last() == '.') { packageName = packageName.Substring(0, packageName.Length-1); } //packageName = packageName + "_" + GetWorkshopItemIDFromUrl(contentPackage.SteamWorkshopUrl); return Path.Combine("Mods", packageName, MetadataFileName); @@ -1421,7 +1507,7 @@ namespace Barotrauma.Steam private static void CorrectContentFileCopy(ContentPackage package, string src, string dest, bool overwrite) { - if (Path.GetExtension(src).ToLowerInvariant() == ".xml") + if (Path.GetExtension(src).Equals(".xml", StringComparison.OrdinalIgnoreCase)) { XDocument doc = XMLExtensions.TryLoadXml(src); if (doc != null) @@ -1471,7 +1557,7 @@ namespace Barotrauma.Steam { if (checkIfFileExists) { - ContentPackage otherContentPackage = ContentPackage.List.Find(cp => cp.Name.ToLowerInvariant() == splitPath[1].ToLowerInvariant()); + ContentPackage otherContentPackage = ContentPackage.List.Find(cp => cp.Name.Equals(splitPath[1], StringComparison.OrdinalIgnoreCase)); if (otherContentPackage != null) { string otherPackageName = Path.GetDirectoryName(otherContentPackage.Path); @@ -1483,7 +1569,8 @@ namespace Barotrauma.Steam } } } - newPath = Path.Combine(packageName, string.Join("/", splitPath.Skip(2))); + splitPath = splitPath.Skip(Math.Clamp(splitPath.Length-1, 0, 2)).ToArray(); + newPath = Path.Combine(packageName, string.Join("/", splitPath)); } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs index bf506cd08..a241aaf76 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipCapture.cs @@ -149,9 +149,15 @@ namespace Barotrauma.Networking } } + short[] uncompressedBuffer = new short[VoipConfig.BUFFER_SIZE]; + short[] prevUncompressedBuffer = new short[VoipConfig.BUFFER_SIZE]; + bool prevCaptured = true; + int captureTimer; + void UpdateCapture() { - short[] uncompressedBuffer = new short[VoipConfig.BUFFER_SIZE]; + Array.Copy(uncompressedBuffer, 0, prevUncompressedBuffer, 0, VoipConfig.BUFFER_SIZE); + Array.Clear(uncompressedBuffer, 0, VoipConfig.BUFFER_SIZE); while (capturing) { int alcError; @@ -202,6 +208,21 @@ namespace Barotrauma.Networking bool allowEnqueue = false; if (GameMain.WindowActive) { + ForceLocal = captureTimer > 0 ? ForceLocal : false; + bool pttDown = false; + if ((PlayerInput.KeyDown(InputType.Voice) || PlayerInput.KeyDown(InputType.LocalVoice)) && + GUI.KeyboardDispatcher.Subscriber == null) + { + pttDown = true; + if (PlayerInput.KeyDown(InputType.LocalVoice)) + { + ForceLocal = true; + } + else + { + ForceLocal = false; + } + } if (GameMain.Config.VoiceSetting == GameSettings.VoiceMode.Activity) { if (dB > GameMain.Config.NoiseGateThreshold) @@ -211,25 +232,38 @@ namespace Barotrauma.Networking } else if (GameMain.Config.VoiceSetting == GameSettings.VoiceMode.PushToTalk) { - if (PlayerInput.KeyDown(InputType.Voice) && GUI.KeyboardDispatcher.Subscriber == null) + if (pttDown) { allowEnqueue = true; } } } - if (allowEnqueue) + if (allowEnqueue || captureTimer > 0) { LastEnqueueAudio = DateTime.Now; //encode audio and enqueue it lock (buffers) { + if (!prevCaptured) //enqueue the previous buffer if not sent to avoid cutoff + { + int compressedCountPrev = VoipConfig.Encoder.Encode(prevUncompressedBuffer, 0, VoipConfig.BUFFER_SIZE, BufferToQueue, 0, VoipConfig.MAX_COMPRESSED_SIZE); + EnqueueBuffer(compressedCountPrev); + } int compressedCount = VoipConfig.Encoder.Encode(uncompressedBuffer, 0, VoipConfig.BUFFER_SIZE, BufferToQueue, 0, VoipConfig.MAX_COMPRESSED_SIZE); EnqueueBuffer(compressedCount); } + captureTimer -= (VoipConfig.BUFFER_SIZE * 1000) / VoipConfig.FREQUENCY; + if (allowEnqueue) + { + captureTimer = GameMain.Config.VoiceChatCutoffPrevention; + } + prevCaptured = true; } else { + captureTimer = 0; + prevCaptured = false; //enqueue silence lock (buffers) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs index 5eab16fff..b648cde9c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs @@ -85,20 +85,20 @@ namespace Barotrauma.Networking return; } - if (queue.Read(msg)) + Client client = gameClient.ConnectedClients.Find(c => c.VoipQueue == queue); + if (queue.Read(msg, discardData: client.Muted || client.MutedLocally)) { - Client client = gameClient.ConnectedClients.Find(c => c.VoipQueue == queue); if (client.Muted || client.MutedLocally) { return; } - if (client.VoipSound == null) { DebugConsole.Log("Recreating voipsound " + queueId); - client.VoipSound = new VoipSound(GameMain.SoundManager, client.VoipQueue); + client.VoipSound = new VoipSound(client.Name, GameMain.SoundManager, client.VoipQueue); } if (client.Character != null && !client.Character.IsDead && !client.Character.Removed && client.Character.SpeechImpediment <= 100.0f) { - var messageType = ChatMessage.CanUseRadio(client.Character, out WifiComponent radio) ? ChatMessageType.Radio : ChatMessageType.Default; + WifiComponent radio = null; + var messageType = !client.VoipQueue.ForceLocal && ChatMessage.CanUseRadio(client.Character, out radio) ? ChatMessageType.Radio : ChatMessageType.Default; client.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); client.VoipSound.UseRadioFilter = messageType == ChatMessageType.Radio; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs index 556265ebf..026f4f287 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs @@ -61,10 +61,10 @@ namespace Barotrauma foreach (GUIComponent comp in listBox.Content.Children) { - if (comp.FindChild("votes") is GUITextBlock voteText) comp.RemoveChild(voteText); + if (comp.FindChild("votes") is GUITextBlock voteText) { comp.RemoveChild(voteText); } } - if (clients == null) return; + if (clients == null) { return; } List> voteList = GetVoteList(voteType, clients); foreach (Pair votable in voteList) @@ -73,7 +73,7 @@ namespace Barotrauma } break; case VoteType.StartRound: - if (clients == null) return; + if (clients == null) { return; } foreach (Client client in clients) { var clientReady = GameMain.NetLobbyScreen.PlayerList.Content.FindChild(client)?.FindChild("clientready"); @@ -113,18 +113,18 @@ namespace Barotrauma switch (voteType) { case VoteType.Sub: - Submarine sub = data as Submarine; - if (sub == null) return; + SubmarineInfo sub = data as SubmarineInfo; + if (sub == null) { return; } msg.Write(sub.Name); break; case VoteType.Mode: GameModePreset gameMode = data as GameModePreset; - if (gameMode == null) return; + if (gameMode == null) { return; } msg.Write(gameMode.Identifier); break; case VoteType.EndRound: - if (!(data is bool)) return; + if (!(data is bool)) { return; } msg.Write((bool)data); break; case VoteType.Kick: @@ -153,12 +153,12 @@ namespace Barotrauma { int votes = inc.ReadByte(); string subName = inc.ReadString(); - List serversubs = new List(); + List serversubs = new List(); foreach (GUIComponent item in GameMain.NetLobbyScreen?.SubList?.Content?.Children) { - if (item.UserData != null && item.UserData is Submarine) serversubs.Add(item.UserData as Submarine); + if (item.UserData != null && item.UserData is SubmarineInfo) { serversubs.Add(item.UserData as SubmarineInfo); } } - Submarine sub = serversubs.FirstOrDefault(sm => sm.Name == subName); + SubmarineInfo sub = serversubs.FirstOrDefault(s => s.Name == subName); SetVoteText(GameMain.NetLobbyScreen.SubList, sub, votes); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/DecalPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/DecalPrefab.cs index d4c179ae1..da75fd497 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/DecalPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/DecalPrefab.cs @@ -11,7 +11,18 @@ namespace Barotrauma.Particles public string OriginalName { get { return Name; } } - public string Identifier { get { return Name.ToLowerInvariant(); } } + private string _identifier; + public string Identifier + { + get + { + if (_identifier == null) + { + _identifier = Name.ToLowerInvariant(); + } + return _identifier; + } + } public string FilePath { get; private set; } @@ -46,7 +57,7 @@ namespace Barotrauma.Particles foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() == "sprite") + if (subElement.Name.ToString().Equals("sprite", StringComparison.OrdinalIgnoreCase)) { Sprites.Add(new Sprite(subElement)); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs index f7471bda7..6a140118c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs @@ -108,7 +108,8 @@ namespace Barotrauma.Particles public readonly bool CopyEntityAngle; - public readonly bool DrawOnTop; + public bool DrawOnTop => forceDrawOnTop || ParticlePrefab.DrawOnTop; + private readonly bool forceDrawOnTop; public ParticleEmitterPrefab(XElement element) { @@ -150,6 +151,12 @@ namespace Barotrauma.Particles { DistanceMin = DistanceMax = element.GetAttributeFloat("distance", 0.0f); } + if (DistanceMax < DistanceMin) + { + var temp = DistanceMin; + DistanceMin = DistanceMax; + DistanceMax = temp; + } if (element.Attribute("velocity") == null) { @@ -160,13 +167,19 @@ namespace Barotrauma.Particles { VelocityMin = VelocityMax = element.GetAttributeFloat("velocity", 0.0f); } + if (VelocityMax < VelocityMin) + { + var temp = VelocityMin; + VelocityMin = VelocityMax; + VelocityMax = temp; + } EmitInterval = element.GetAttributeFloat("emitinterval", 0.0f); ParticlesPerSecond = element.GetAttributeInt("particlespersecond", 0); ParticleAmount = element.GetAttributeInt("particleamount", 0); HighQualityCollisionDetection = element.GetAttributeBool("highqualitycollisiondetection", false); CopyEntityAngle = element.GetAttributeBool("copyentityangle", false); - DrawOnTop = element.GetAttributeBool("drawontop", false); + forceDrawOnTop = element.GetAttributeBool("drawontop", false); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs index c8c31dd77..4666696e9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleManager.cs @@ -228,14 +228,7 @@ namespace Barotrauma.Particles if (inSub.HasValue) { bool isOutside = particle.CurrentHull == null; - if (particle.DrawOnTop) - { - if (isOutside != inSub.Value) - { - continue; - } - } - else if (isOutside == inSub.Value) + if (!particle.DrawOnTop && isOutside == inSub.Value) { continue; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs index 067269913..9a0cb6a6c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticlePrefab.cs @@ -199,6 +199,9 @@ namespace Barotrauma.Particles [Editable, Serialize(DrawTargetType.Air, false, description: "Should the particle be rendered in air, water or both.")] public DrawTargetType DrawTarget { get; private set; } + [Editable, Serialize(false, false, description: "Should the particle be always rendered on top of entities?")] + public bool DrawOnTop { get; private set; } + [Editable, Serialize(ParticleBlendState.AlphaBlend, false, description: "The type of blending to use when rendering the particle.")] public ParticleBlendState BlendState { get; private set; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs b/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs index 5e2f6e3bb..4957b69c5 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/PlayerInput.cs @@ -418,6 +418,16 @@ namespace Barotrauma return AllowInput && keyboardState.IsKeyUp(button); } + public static bool IsShiftDown() + { + return KeyDown(Keys.LeftShift) || KeyDown(Keys.RightShift); + } + + public static bool IsCtrlDown() + { + return KeyDown(Keys.LeftControl) || KeyDown(Keys.RightControl); + } + public static void Update(double deltaTime) { timeSinceClick += deltaTime; @@ -448,6 +458,7 @@ namespace Barotrauma MouseSpeedPerSecond = MouseSpeed / (float)deltaTime; + // Split into two to not accept drag & drop releasing as part of a double-click doubleClicked = false; if (PrimaryMouseButtonClicked()) { @@ -455,9 +466,22 @@ namespace Barotrauma (mouseState.Position - lastClickPosition).ToVector2().Length() < MaxDoubleClickDistance) { doubleClicked = true; + timeSinceClick = DoubleClickDelay; } - lastClickPosition = mouseState.Position; + else if (timeSinceClick < DoubleClickDelay) + { + lastClickPosition = mouseState.Position; + } + timeSinceClick = 0.0; + } + + if (PrimaryMouseButtonDown()) + { + if (timeSinceClick > DoubleClickDelay) + { + lastClickPosition = mouseState.Position; + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Program.cs b/Barotrauma/BarotraumaClient/ClientSource/Program.cs index ed4a8a0dc..6ecf9226b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Program.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Program.cs @@ -8,6 +8,7 @@ using GameAnalyticsSDK.Net; using Barotrauma.Steam; using System.Diagnostics; using System.Runtime.InteropServices; +using System.Xml.Linq; #if WINDOWS using SharpDX; @@ -110,6 +111,33 @@ namespace Barotrauma sb.AppendLine("\n"); sb.AppendLine("Barotrauma seems to have crashed. Sorry for the inconvenience! "); sb.AppendLine("\n"); + + try + { + if (exception is GameMain.LoadingException) + { + //exception occurred in loading screen: + //assume content packages are the culprit and reset them + XDocument doc = XMLExtensions.TryLoadXml(GameSettings.PlayerSavePath); + XDocument baseDoc = XMLExtensions.TryLoadXml(GameSettings.SavePath); + if (doc != null && baseDoc != null) + { + XElement newElement = new XElement(doc.Root.Name); + newElement.Add(doc.Root.Attributes()); + newElement.Add(doc.Root.Elements().Where(e => !e.Name.LocalName.Equals("contentpackage", StringComparison.InvariantCultureIgnoreCase))); + newElement.Add(baseDoc.Root.Elements().Where(e => e.Name.LocalName.Equals("contentpackage", StringComparison.InvariantCultureIgnoreCase))); + XDocument newDoc = new XDocument(newElement); + newDoc.Save(GameSettings.PlayerSavePath); + sb.AppendLine("To prevent further startup errors, installed mods will be disabled the next time you launch the game."); + sb.AppendLine("\n"); + } + } + } + catch + { + //welp i guess we couldn't reset the config! + } + if (exeHash?.Hash != null) { sb.AppendLine(exeHash.Hash); @@ -127,7 +155,7 @@ namespace Barotrauma sb.AppendLine("Selected content packages: " + (!GameMain.SelectedPackages.Any() ? "None" : string.Join(", ", GameMain.SelectedPackages.Select(c => c.Name)))); } sb.AppendLine("Level seed: " + ((Level.Loaded == null) ? "no level loaded" : Level.Loaded.Seed)); - sb.AppendLine("Loaded submarine: " + ((Submarine.MainSub == null) ? "None" : Submarine.MainSub.Name + " (" + Submarine.MainSub.MD5Hash + ")")); + sb.AppendLine("Loaded submarine: " + ((Submarine.MainSub == null) ? "None" : Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash + ")")); sb.AppendLine("Selected screen: " + (Screen.Selected == null ? "None" : Screen.Selected.ToString())); if (SteamManager.IsInitialized) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs index a19c776a3..37e835f56 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignSetupUI.cs @@ -21,7 +21,7 @@ namespace Barotrauma private GUIButton loadGameButton, deleteMpSaveButton; - public Action StartNewGame; + public Action StartNewGame; public Action LoadGame; public GUIButton StartButton @@ -32,7 +32,7 @@ namespace Barotrauma private readonly bool isMultiplayer; - public CampaignSetupUI(bool isMultiplayer, GUIComponent newGameContainer, GUIComponent loadGameContainer, IEnumerable submarines, IEnumerable saveFiles = null) + public CampaignSetupUI(bool isMultiplayer, GUIComponent newGameContainer, GUIComponent loadGameContainer, IEnumerable submarines, IEnumerable saveFiles = null) { this.isMultiplayer = isMultiplayer; this.newGameContainer = newGameContainer; @@ -81,6 +81,7 @@ namespace Barotrauma var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUI.Font); var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), filterContainer.RectTransform, Anchor.CenterRight), font: GUI.Font, createClearButton: true); + filterContainer.RectTransform.MinSize = searchBox.RectTransform.MinSize; searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; searchBox.OnTextChanged += (textBox, text) => { FilterSubs(subList, text); return true; }; @@ -115,12 +116,12 @@ namespace Barotrauma return false; } - Submarine selectedSub = null; + SubmarineInfo selectedSub = null; if (!isMultiplayer) { - if (!(subList.SelectedData is Submarine)) { return false; } - selectedSub = subList.SelectedData as Submarine; + if (!(subList.SelectedData is SubmarineInfo)) { return false; } + selectedSub = subList.SelectedData as SubmarineInfo; } else { @@ -226,7 +227,7 @@ namespace Barotrauma { foreach (GUIComponent child in subList.Content.Children) { - var sub = child.UserData as Submarine; + var sub = child.UserData as SubmarineInfo; if (sub == null) { return; } child.Visible = string.IsNullOrEmpty(filter) ? true : sub.DisplayName.ToLower().Contains(filter.ToLower()); } @@ -238,7 +239,7 @@ namespace Barotrauma (subPreviewContainer.Parent as GUILayoutGroup)?.Recalculate(); subPreviewContainer.ClearChildren(); - Submarine sub = obj as Submarine; + SubmarineInfo sub = obj as SubmarineInfo; if (sub == null) { return true; } sub.CreatePreviewWindow(subPreviewContainer); @@ -278,7 +279,7 @@ namespace Barotrauma saveNameBox.Text = Path.GetFileNameWithoutExtension(savePath); } - public void UpdateSubList(IEnumerable submarines) + public void UpdateSubList(IEnumerable submarines) { #if !DEBUG var subsToShow = submarines.Where(s => !s.HasTag(SubmarineTag.HideInMenus)); @@ -288,7 +289,7 @@ namespace Barotrauma subList.ClearChildren(); - foreach (Submarine sub in subsToShow) + foreach (SubmarineInfo sub in subsToShow) { var textBlock = new GUITextBlock( new RectTransform(new Vector2(1, 0.1f), subList.Content.RectTransform) { MinSize = new Point(0, 30) }, @@ -319,7 +320,7 @@ namespace Barotrauma }; } } - if (Submarine.SavedSubmarines.Any()) + if (SubmarineInfo.SavedSubmarines.Any()) { var nonShuttles = subsToShow.Where(s => !s.HasTag(SubmarineTag.Shuttle)).ToList(); if (nonShuttles.Count > 0) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index e05f157a0..ab4b6ac92 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -435,8 +435,8 @@ namespace Barotrauma { OnClicked = (btn, userdata) => { - if (GameMain.GameSession?.Submarine != null && - GameMain.GameSession.Submarine.LeftBehindSubDockingPortOccupied) + if (GameMain.GameSession?.SubmarineInfo != null && + GameMain.GameSession.SubmarineInfo.LeftBehindSubDockingPortOccupied) { new GUIMessageBox("", TextManager.Get("ReplaceShuttleDockingPortOccupied")); return true; @@ -917,7 +917,7 @@ namespace Barotrauma GUINumberInput.NumberType.Int) { MinValueInt = 0, - MaxValueInt = 100, + MaxValueInt = CargoManager.MaxQuantity, UserData = pi, IntValue = pi.Quantity }; @@ -927,7 +927,11 @@ namespace Barotrauma { if (suppressBuySell) { return; } PurchasedItem purchasedItem = numberInput.UserData as PurchasedItem; - + if (GameMain.Client != null && !GameMain.Client.HasPermission(Networking.ClientPermissions.ManageCampaign)) + { + numberInput.IntValue = purchasedItem.Quantity; + return; + } //Attempting to buy if (numberInput.IntValue > purchasedItem.Quantity) { @@ -965,15 +969,18 @@ namespace Barotrauma private bool BuyItem(GUIComponent component, object obj) { - if (!(obj is PurchasedItem pi) || pi.ItemPrefab == null) return false; + if (!(obj is PurchasedItem pi) || pi.ItemPrefab == null) { return false; } if (GameMain.Client != null && !GameMain.Client.HasPermission(Networking.ClientPermissions.ManageCampaign)) { return false; } - + + var purchasedItem = Campaign.CargoManager.PurchasedItems.Find(pi2 => pi2.ItemPrefab == pi.ItemPrefab); + if (purchasedItem != null && purchasedItem.Quantity >= CargoManager.MaxQuantity) { return false; } + PriceInfo priceInfo = pi.ItemPrefab.GetPrice(Campaign.Map.CurrentLocation); - if (priceInfo == null || priceInfo.BuyPrice > Campaign.Money) return false; + if (priceInfo == null || priceInfo.BuyPrice > Campaign.Money) { return false; } Campaign.CargoManager.PurchaseItem(pi.ItemPrefab, 1); GameMain.Client?.SendCampaignState(); @@ -983,7 +990,7 @@ namespace Barotrauma private bool SellItem(GUIComponent component, object obj) { - if (!(obj is PurchasedItem pi) || pi.ItemPrefab == null) return false; + if (!(obj is PurchasedItem pi) || pi.ItemPrefab == null) { return false; } if (GameMain.Client != null && !GameMain.Client.HasPermission(Networking.ClientPermissions.ManageCampaign)) { @@ -1068,7 +1075,7 @@ namespace Barotrauma (GameMain.Client == null || GameMain.Client.HasPermission(Networking.ClientPermissions.ManageCampaign)); repairItemsButton.GetChild().Selected = Campaign.PurchasedItemRepairs; - if (GameMain.GameSession?.Submarine == null || !GameMain.GameSession.Submarine.SubsLeftBehind) + if (GameMain.GameSession?.SubmarineInfo == null || !GameMain.GameSession.SubmarineInfo.SubsLeftBehind) { replaceShuttlesButton.Enabled = false; replaceShuttlesButton.GetChild().Selected = false; @@ -1166,7 +1173,7 @@ namespace Barotrauma }; var characterPreviewContent = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.8f), characterPreviewFrame.RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.02f) }, style: null); - characterInfo.CreateInfoFrame(characterPreviewContent); + characterInfo.CreateInfoFrame(characterPreviewContent, true); } var currentCrew = GameMain.GameSession.CrewManager.GetCharacterInfos(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs index fd0284e2f..67b360ed4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/CharacterEditorScreen.cs @@ -119,9 +119,12 @@ namespace Barotrauma.CharacterEditor if (Submarine.MainSub == null) { ResetVariables(); - Submarine.MainSub = new Submarine("Content/AnimEditor.sub"); - Submarine.MainSub.Load(unloadPrevious: false, showWarningMessages: false); - Submarine.MainSub.PhysicsBody.Enabled = false; + var subInfo = new SubmarineInfo("Content/AnimEditor.sub"); + Submarine.MainSub = new Submarine(subInfo); + if (Submarine.MainSub.PhysicsBody != null) + { + Submarine.MainSub.PhysicsBody.Enabled = false; + } originalWall = new WallGroup(new List(Structure.WallList)); CloneWalls(); CalculateMovementLimits(); @@ -476,9 +479,16 @@ namespace Barotrauma.CharacterEditor if (character.IsHumanoid) { animTestPoseToggle.Enabled = CurrentAnimation.IsGroundedAnimation; - if (animTestPoseToggle.Enabled && PlayerInput.KeyHit(Keys.X)) + if (animTestPoseToggle.Enabled) { - SetToggle(animTestPoseToggle, !animTestPoseToggle.Selected); + if (PlayerInput.KeyHit(Keys.X)) + { + SetToggle(animTestPoseToggle, !animTestPoseToggle.Selected); + } + } + else + { + animTestPoseToggle.Selected = false; } } if (PlayerInput.KeyHit(InputType.Run)) @@ -2117,7 +2127,7 @@ namespace Barotrauma.CharacterEditor CreateTextures(); return true; }; - new GUIButton(new RectTransform(buttonSize, parent.RectTransform, Anchor.BottomCenter), GetCharacterEditorTranslation("RecreateRagdoll")) + var recreateButton = new GUIButton(new RectTransform(buttonSize, parent.RectTransform, Anchor.BottomCenter), GetCharacterEditorTranslation("RecreateRagdoll")) { ToolTip = GetCharacterEditorTranslation("RecreateRagdollTooltip"), OnClicked = (button, data) => @@ -2127,6 +2137,7 @@ namespace Barotrauma.CharacterEditor return true; } }; + GUITextBlock.AutoScaleAndNormalize(reloadTexturesButton.TextBlock, recreateButton.TextBlock); buttonsPanelToggle = new ToggleButton(new RectTransform(new Vector2(0.08f, 1), buttonsPanel.RectTransform, Anchor.CenterRight, Pivot.CenterLeft), Direction.Left); buttonsPanel.RectTransform.MinSize = new Point(0, (int)(parent.RectTransform.Children.Sum(c => c.MinSize.Y) * 1.5f)); } @@ -3117,6 +3128,8 @@ namespace Barotrauma.CharacterEditor } }; + GUITextBlock.AutoScaleAndNormalize(layoutGroup.Children.Where(c => c is GUIButton).Select(c => ((GUIButton)c).TextBlock)); + fileEditToggle = new ToggleButton(new RectTransform(new Vector2(0.08f, 1), fileEditPanel.RectTransform, Anchor.CenterLeft, Pivot.CenterRight), Direction.Right); void ResetView() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs index ba788163d..8f6eb15ce 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs @@ -394,7 +394,7 @@ namespace Barotrauma.CharacterEditor return false; } var path = Path.GetFileName(TexturePath); - if (!path.EndsWith(".png", StringComparison.InvariantCultureIgnoreCase)) + if (!path.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) { GUI.AddMessage(TextManager.Get("WrongFileType"), GUI.Style.Red); texturePathElement.Flash(GUI.Style.Red); @@ -724,8 +724,8 @@ namespace Barotrauma.CharacterEditor { ParseLimbsFromGUIElements(); ParseJointsFromGUIElements(); - var main = LimbXElements.Values.Select(xe => xe.Attribute("type")).Where(a => a.Value.ToLowerInvariant() == "torso").FirstOrDefault() ?? - LimbXElements.Values.Select(xe => xe.Attribute("type")).Where(a => a.Value.ToLowerInvariant() == "head").FirstOrDefault(); + var main = LimbXElements.Values.Select(xe => xe.Attribute("type")).Where(a => a.Value.Equals("torso", StringComparison.OrdinalIgnoreCase)).FirstOrDefault() ?? + LimbXElements.Values.Select(xe => xe.Attribute("type")).Where(a => a.Value.Equals("head", StringComparison.OrdinalIgnoreCase)).FirstOrDefault(); if (main == null) { GUI.AddMessage(GetCharacterEditorTranslation("MissingTorsoOrHead"), GUI.Style.Red); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs index 3a0e2d267..c35d3caae 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/GameScreen.cs @@ -17,7 +17,7 @@ namespace Barotrauma private RenderTarget2D renderTargetFinal; private Effect damageEffect; - private Texture2D damageStencil; + private Texture2D damageStencil; private Texture2D distortTexture; public Effect PostProcessEffect { get; private set; } @@ -122,10 +122,12 @@ namespace Barotrauma { if (Submarine.MainSubs[i] == null) continue; if (Level.Loaded != null && Submarine.MainSubs[i].WorldPosition.Y < Level.MaxEntityDepth) continue; - + + Vector2 position = Submarine.MainSubs[i].SubBody != null ? Submarine.MainSubs[i].WorldPosition : Submarine.MainSubs[i].HiddenSubPosition; + Color indicatorColor = i == 0 ? Color.LightBlue * 0.5f : GUI.Style.Red * 0.5f; GUI.DrawIndicator( - spriteBatch, Submarine.MainSubs[i].WorldPosition, cam, + spriteBatch, position, cam, Math.Max(Submarine.MainSub.Borders.Width, Submarine.MainSub.Borders.Height), GUI.SubmarineIcon, indicatorColor); } @@ -202,9 +204,12 @@ namespace Barotrauma //Start drawing to the normal render target (stuff that can't be seen through the LOS effect) graphics.SetRenderTarget(renderTarget); - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, null); - spriteBatch.Draw(renderTargetBackground, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), Color.White); - spriteBatch.End(); + + graphics.BlendState = BlendState.NonPremultiplied; + graphics.SamplerStates[0] = SamplerState.LinearWrap; + Quad.UseBasicEffect(renderTargetBackground); + Quad.Render(); + //Draw the rest of the structures, characters and front structures spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, null, DepthStencilState.None, null, null, cam.Transform); Submarine.DrawBack(spriteBatch, false, e => !(e is Structure) || e.SpriteDepth < 0.9f); @@ -230,11 +235,12 @@ namespace Barotrauma //draw the rendertarget and particles that are only supposed to be drawn in water into renderTargetWater graphics.SetRenderTarget(renderTargetWater); - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Opaque); - spriteBatch.Draw(renderTarget, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), Color.White);// waterColor); - spriteBatch.End(); + graphics.BlendState = BlendState.Opaque; + graphics.SamplerStates[0] = SamplerState.LinearWrap; + Quad.UseBasicEffect(renderTarget); + Quad.Render(); - //draw alpha blended particles that are inside a sub + //draw alpha blended particles that are inside a sub spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, null, DepthStencilState.DepthRead, null, null, cam.Transform); GameMain.ParticleManager.Draw(spriteBatch, true, true, Particles.ParticleBlendState.AlphaBlend); spriteBatch.End(); @@ -282,10 +288,12 @@ namespace Barotrauma spriteBatch.End(); if (GameMain.LightManager.LightingEnabled) { - spriteBatch.Begin(SpriteSortMode.Deferred, Lights.CustomBlendStates.Multiplicative, null, DepthStencilState.None, null, null, null); - spriteBatch.Draw(GameMain.LightManager.LightMap, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), Color.White); - spriteBatch.End(); - } + graphics.DepthStencilState = DepthStencilState.None; + graphics.SamplerStates[0] = SamplerState.LinearWrap; + graphics.BlendState = Lights.CustomBlendStates.Multiplicative; + Quad.UseBasicEffect(GameMain.LightManager.LightMap); + Quad.Render(); + } spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.LinearWrap, DepthStencilState.None, null, null, cam.Transform); foreach (Character c in Character.CharacterList) @@ -331,9 +339,12 @@ namespace Barotrauma losColor = Color.Black; } - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.PointClamp, null, null, GameMain.LightManager.LosEffect, null); - spriteBatch.Draw(renderTargetBackground, new Rectangle(0, 0, spriteBatch.GraphicsDevice.Viewport.Width, spriteBatch.GraphicsDevice.Viewport.Height), losColor); - spriteBatch.End(); + GameMain.LightManager.LosEffect.Parameters["xColor"].SetValue(losColor.ToVector4()); + + graphics.BlendState = BlendState.NonPremultiplied; + graphics.SamplerStates[0] = SamplerState.PointClamp; + GameMain.LightManager.LosEffect.CurrentTechnique.Passes[0].Apply(); + Quad.Render(); } graphics.SetRenderTarget(null); @@ -371,29 +382,23 @@ namespace Barotrauma postProcessTechnique += "Distort"; PostProcessEffect.Parameters["distortScale"].SetValue(Vector2.One * DistortStrength); PostProcessEffect.Parameters["distortUvOffset"].SetValue(WaterRenderer.Instance.WavePos * 0.001f); -#if LINUX || OSX - PostProcessEffect.Parameters["xTexture"].SetValue(distortTexture); -#else - PostProcessEffect.Parameters["xTexture"].SetValue(renderTargetFinal); -#endif } + graphics.BlendState = BlendState.Opaque; + graphics.SamplerStates[0] = SamplerState.LinearClamp; + graphics.DepthStencilState = DepthStencilState.None; if (string.IsNullOrEmpty(postProcessTechnique)) { - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Opaque, SamplerState.PointClamp, DepthStencilState.None); + Quad.UseBasicEffect(renderTargetFinal); } else { + PostProcessEffect.Parameters["MatrixTransform"].SetValue(Matrix.Identity); + PostProcessEffect.Parameters["xTexture"].SetValue(renderTargetFinal); PostProcessEffect.CurrentTechnique = PostProcessEffect.Techniques[postProcessTechnique]; PostProcessEffect.CurrentTechnique.Passes[0].Apply(); - spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Opaque, SamplerState.PointClamp, DepthStencilState.None, effect: PostProcessEffect); } -#if LINUX || OSX - spriteBatch.Draw(renderTargetFinal, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), Color.White); -#else - spriteBatch.Draw(DistortStrength > 0.0f ? distortTexture : renderTargetFinal, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), Color.White); -#endif - spriteBatch.End(); + Quad.Render(); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs index a89bb23c3..a56ac3f40 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LevelEditorScreen.cs @@ -170,14 +170,6 @@ namespace Barotrauma { base.Select(); - foreach (LevelObjectPrefab levelObjPrefab in LevelObjectPrefab.List) - { - foreach (Sprite sprite in levelObjPrefab.Sprites) - { - sprite?.EnsureLazyLoaded(); - } - } - pointerLightSource = new LightSource(Vector2.Zero, 1000.0f, Color.White, submarine: null); GameMain.LightManager.AddLight(pointerLightSource); topPanel.ClearChildren(); @@ -253,9 +245,10 @@ namespace Barotrauma }; Sprite sprite = levelObjPrefab.Sprites.FirstOrDefault() ?? levelObjPrefab.DeformableSprite?.Sprite; - GUIImage img = new GUIImage(new RectTransform(new Point(paddedFrame.Rect.Height, paddedFrame.Rect.Height - textBlock.Rect.Height), + new GUIImage(new RectTransform(new Point(paddedFrame.Rect.Height, paddedFrame.Rect.Height - textBlock.Rect.Height), paddedFrame.RectTransform, Anchor.TopCenter), sprite, scaleToFit: true) { + LoadAsynchronously = true, CanBeFocused = false }; } @@ -466,6 +459,7 @@ namespace Barotrauma Submarine.Draw(spriteBatch, false); Submarine.DrawFront(spriteBatch); Submarine.DrawDamageable(spriteBatch, null); + GUI.DrawRectangle(spriteBatch, new Rectangle(new Point(0, -Level.Loaded.Size.Y), Level.Loaded.Size), Color.White, thickness: (int)(1.0f / cam.Zoom)); spriteBatch.End(); if (lightingEnabled.Selected) @@ -531,7 +525,7 @@ namespace Barotrauma else if (element.Name.ToString().Equals(genParams.Name, StringComparison.OrdinalIgnoreCase)) { SerializableProperty.SerializeProperties(genParams, element, true); - } + } break; } } @@ -552,7 +546,7 @@ namespace Barotrauma { foreach (XElement element in doc.Root.Elements()) { - if (element.Name.ToString().ToLowerInvariant() != levelObjPrefab.Name.ToLowerInvariant()) continue; + if (!element.Name.ToString().Equals(levelObjPrefab.Name, StringComparison.OrdinalIgnoreCase)) { continue; } levelObjPrefab.Save(element); break; } @@ -577,7 +571,7 @@ namespace Barotrauma bool elementFound = false; foreach (XElement element in doc.Root.Elements()) { - if (element.Name.ToString().ToLowerInvariant() != genParams.Name.ToLowerInvariant()) continue; + if (!element.Name.ToString().Equals(genParams.Name, StringComparison.OrdinalIgnoreCase)) { continue; } SerializableProperty.SerializeProperties(genParams, element, true); elementFound = true; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/LobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/LobbyScreen.cs index da10f626d..52b9b2f26 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/LobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/LobbyScreen.cs @@ -78,8 +78,7 @@ namespace Barotrauma private IEnumerable LoadRound() { - GameMain.GameSession.StartRound(campaignUI.SelectedLevel, - reloadSub: true, + GameMain.GameSession.StartRound(campaignUI.SelectedLevel, mirrorLevel: GameMain.GameSession.Map.CurrentLocation != GameMain.GameSession.Map.SelectedConnection.Locations[0]); GameMain.GameScreen.Select(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index d8144f7e3..fdea888d7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -31,6 +31,7 @@ namespace Barotrauma private GUITextBox serverNameBox, /*portBox, queryPortBox,*/ passwordBox, maxPlayersBox; private GUITickBox isPublicBox, wrongPasswordBanBox, karmaEnabledBox; private GUIDropDown karmaPresetDD; + private readonly GUIFrame downloadingModsContainer, enableModsContainer; private readonly GUIButton joinServerButton, hostServerButton, steamWorkshopButton; private readonly GameMain game; @@ -230,13 +231,31 @@ namespace Barotrauma }; #if USE_STEAM - steamWorkshopButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), customizeList.RectTransform), TextManager.Get("SteamWorkshopButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") + var steamWorkshopButtonContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.0f), customizeList.RectTransform), style: null); + + steamWorkshopButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), steamWorkshopButtonContainer.RectTransform), TextManager.Get("SteamWorkshopButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { ForceUpperCase = true, Enabled = false, UserData = Tab.SteamWorkshop, OnClicked = SelectTab }; + + downloadingModsContainer = new GUIFrame(new RectTransform(new Vector2(1.4f, 0.9f), steamWorkshopButtonContainer.RectTransform, + Anchor.CenterRight, Pivot.CenterLeft) + { RelativeOffset = new Vector2(0.3f, 0.0f) }, + "MainMenuNotifBackground", Color.Yellow) + { + CanBeFocused = false, + UserData = "workshopnotif", + Visible = false + }; + new GUITextBlock(new RectTransform(Vector2.One * 0.9f, downloadingModsContainer.RectTransform, Anchor.CenterLeft, Pivot.CenterLeft) { RelativeOffset = new Vector2(0.05f, 0.0f) }, + TextManager.Get("ModsDownloadingNotif"), Color.Black) + { + CanBeFocused = false, + }; + #endif new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), customizeList.RectTransform), TextManager.Get("SubEditorButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") @@ -280,12 +299,28 @@ namespace Barotrauma RelativeSpacing = 0.035f }; - new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), optionList.RectTransform), TextManager.Get("SettingsButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") + var settingsButtonContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 1.0f), optionList.RectTransform), style: null); + + new GUIButton(new RectTransform(Vector2.One, settingsButtonContainer.RectTransform), TextManager.Get("SettingsButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { ForceUpperCase = true, UserData = Tab.Settings, OnClicked = SelectTab }; + + enableModsContainer = new GUIFrame(new RectTransform(new Vector2(1.4f, 0.9f), settingsButtonContainer.RectTransform, + Anchor.CenterRight, Pivot.CenterLeft) { RelativeOffset = new Vector2(0.5f, 0.0f) }, + "MainMenuNotifBackground", Color.Yellow) + { + CanBeFocused = false, + UserData = "settingsnotif", + Visible = false + }; + new GUITextBlock(new RectTransform(Vector2.One * 0.9f, enableModsContainer.RectTransform, Anchor.CenterLeft, Pivot.CenterLeft) { RelativeOffset = new Vector2(0.05f, 0.0f) }, + TextManager.Get("ModsInstalledNotif"), Color.Black) + { + CanBeFocused = false + }; new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), optionList.RectTransform), TextManager.Get("CreditsButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { @@ -401,12 +436,18 @@ namespace Barotrauma GameMain.Client = null; } + GameMain.SubEditorScreen?.ClearBackedUpSubInfo(); Submarine.Unload(); ResetButtonStates(null); GameAnalyticsManager.SetCustomDimension01(""); + if (GameMain.SteamWorkshopScreen != null) + { + CoroutineManager.StartCoroutine(GameMain.SteamWorkshopScreen.RefreshDownloadState()); + } + #if OSX // Hack for adjusting the viewport properly after splash screens on older Macs if (firstLoadOnMac) @@ -495,12 +536,13 @@ namespace Barotrauma } campaignSetupUI.CreateDefaultSaveName(); campaignSetupUI.RandomizeSeed(); - campaignSetupUI.UpdateSubList(Submarine.SavedSubmarines); + campaignSetupUI.UpdateSubList(SubmarineInfo.SavedSubmarines); break; case Tab.LoadGame: campaignSetupUI.UpdateLoadMenu(); break; case Tab.Settings: + GameMain.MainMenuScreen?.SetEnableModsNotification(false); menuTabs[(int)Tab.Settings].RectTransform.ClearChildren(); GameMain.Config.SettingsFrame.RectTransform.Parent = menuTabs[(int)Tab.Settings].RectTransform; GameMain.Config.SettingsFrame.RectTransform.RelativeSize = Vector2.One; @@ -631,12 +673,12 @@ namespace Barotrauma Rand.SetLocalRandom(1); } - Submarine selectedSub = null; + SubmarineInfo selectedSub = null; string subName = GameMain.Config.QuickStartSubmarineName; if (!string.IsNullOrEmpty(subName)) { DebugConsole.NewMessage($"Loading the predefined quick start sub \"{subName}\"", Color.White); - selectedSub = Submarine.SavedSubmarines.FirstOrDefault(s => + selectedSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name.ToLower() == subName.ToLower()); if (selectedSub == null) @@ -647,7 +689,7 @@ namespace Barotrauma if (selectedSub == null) { DebugConsole.NewMessage("Loading a random sub.", Color.White); - var subs = Submarine.SavedSubmarines.Where(s => !s.HasTag(SubmarineTag.Shuttle) && !s.HasTag(SubmarineTag.HideInMenus)); + var subs = SubmarineInfo.SavedSubmarines.Where(s => !s.HasTag(SubmarineTag.Shuttle) && !s.HasTag(SubmarineTag.HideInMenus)); selectedSub = subs.ElementAt(Rand.Int(subs.Count())); } var gamesession = new GameSession( @@ -684,6 +726,16 @@ namespace Barotrauma } } + public void SetEnableModsNotification(bool visible) + { + if (enableModsContainer != null) { enableModsContainer.Visible = visible; } + } + + public void SetDownloadingModsNotification(bool visible) + { + if (downloadingModsContainer != null) { downloadingModsContainer.Visible = visible; } + } + private void ShowTutorialSkipWarning(Tab tabToContinueTo) { var tutorialSkipWarning = new GUIMessageBox("", TextManager.Get("tutorialskipwarning"), new string[] { TextManager.Get("tutorialwarningskiptutorials"), TextManager.Get("tutorialwarningplaytutorials") }); @@ -990,7 +1042,7 @@ namespace Barotrauma spriteBatch.End(); } - private void StartGame(Submarine selectedSub, string saveName, string mapSeed) + private void StartGame(SubmarineInfo selectedSub, string saveName, string mapSeed) { if (string.IsNullOrEmpty(saveName)) return; @@ -1027,7 +1079,7 @@ namespace Barotrauma return; } - selectedSub = new Submarine(Path.Combine(SaveUtil.TempPath, selectedSub.Name + ".sub"), ""); + selectedSub = new SubmarineInfo(Path.Combine(SaveUtil.TempPath, selectedSub.Name + ".sub")); GameMain.GameSession = new GameSession(selectedSub, saveName, GameModePreset.List.Find(g => g.Identifier == "singleplayercampaign")); @@ -1072,7 +1124,7 @@ namespace Barotrauma var paddedLoadGame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), menuTabs[(int)Tab.LoadGame].RectTransform, Anchor.Center) { AbsoluteOffset = new Point(0, 10) }, style: null); - campaignSetupUI = new CampaignSetupUI(false, paddedNewGame, paddedLoadGame, Submarine.SavedSubmarines) + campaignSetupUI = new CampaignSetupUI(false, paddedNewGame, paddedLoadGame, SubmarineInfo.SavedSubmarines) { LoadGame = LoadGame, StartNewGame = StartGame diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index bf274c7d5..d184e3ae9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -22,6 +22,7 @@ namespace Barotrauma private GUIListBox subList, modeList; private GUIListBox chatBox, playerList; + private GUIButton serverLogReverseButton; private GUIListBox serverLogBox, serverLogFilterTicks; private GUIComponent jobVariantTooltip; @@ -64,12 +65,15 @@ namespace Barotrauma private readonly GUIButton gameModeViewButton, campaignViewButton, spectateButton; private readonly GUILayoutGroup roundControlsHolder; public GUIButton SettingsButton { get; private set; } + public static GUIButton JobInfoFrame; private readonly GUITickBox spectateBox; private readonly GUIFrame playerInfoContainer; - private GUIButton jobInfoFrame; - private GUIButton playerFrame; + + private GUILayoutGroup infoContainer; + private GUITextBlock changesPendingText; + public GUIButton PlayerFrame; private readonly GUIComponent subPreviewContainer; @@ -208,15 +212,15 @@ namespace Barotrauma private set; } - public Submarine SelectedSub + public SubmarineInfo SelectedSub { - get { return subList.SelectedData as Submarine; } + get { return subList.SelectedData as SubmarineInfo; } set { subList.Select(value); } } - public Submarine SelectedShuttle + public SubmarineInfo SelectedShuttle { - get { return shuttleList.SelectedData as Submarine; } + get { return shuttleList.SelectedData as SubmarineInfo; } } public bool UsingShuttle @@ -525,7 +529,7 @@ namespace Barotrauma if (!(serverLogHolder?.Visible ?? true)) { serverLogHolder.Visible = true; - GameMain.Client.ServerSettings.ServerLog.AssignLogFrame(serverLogBox, serverLogFilterTicks.Content, serverLogFilter); + GameMain.Client.ServerSettings.ServerLog.AssignLogFrame(serverLogReverseButton, serverLogBox, serverLogFilterTicks.Content, serverLogFilter); } showChatButton.Selected = false; showLogButton.Selected = true; @@ -609,7 +613,13 @@ namespace Barotrauma //server log ---------------------------------------------------------------------- - serverLogBox = new GUIListBox(new RectTransform(new Vector2(0.5f, 1.0f), serverLogHolderHorizontal.RectTransform)); + GUILayoutGroup serverLogListboxLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), serverLogHolderHorizontal.RectTransform)) + { + Stretch = true + }; + + serverLogReverseButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), serverLogListboxLayout.RectTransform), style: "UIToggleButtonVertical"); + serverLogBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.95f), serverLogListboxLayout.RectTransform)); //filter tickbox list ------------------------------------------------------------------ @@ -1393,18 +1403,34 @@ namespace Barotrauma parent.ClearChildren(); - GUILayoutGroup infoContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), parent.RectTransform, Anchor.BottomCenter), childAnchor: Anchor.TopCenter) + bool isGameRunning = GameMain.GameSession?.GameMode?.IsRunning ?? false; + + infoContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, isGameRunning ? 0.95f : 0.9f), parent.RectTransform, Anchor.BottomCenter), childAnchor: Anchor.TopCenter) { - RelativeSpacing = 0.015f, + RelativeSpacing = 0.025f, Stretch = true, - UserData = characterInfo + UserData = characterInfo }; - CharacterNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.065f), infoContainer.RectTransform), characterInfo.Name, textAlignment: Alignment.Center) + bool nameChangePending = isGameRunning && GameMain.Client.PendingName != string.Empty && GameMain.Client?.Character?.Name != GameMain.Client.PendingName; + changesPendingText = null; + + if (isGameRunning) + { + infoContainer.RectTransform.AbsoluteOffset = new Point(0, (int)(parent.Rect.Height * 0.025f)); + } + + if (TabMenu.PendingChanges) + { + CreateChangesPendingText(); + } + + CharacterNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.065f), infoContainer.RectTransform), !nameChangePending ? characterInfo.Name : GameMain.Client.PendingName, textAlignment: Alignment.Center) { MaxTextLength = Client.MaxNameLength, OverflowClip = true }; + CharacterNameBox.OnEnterPressed += (tb, text) => { CharacterNameBox.Deselect(); return true; }; CharacterNameBox.OnDeselected += (tb, key) => { @@ -1417,7 +1443,15 @@ namespace Barotrauma } else { - ReadyToStartBox.Selected = false; + if (isGameRunning) + { + GameMain.Client.PendingName = tb.Text; + } + else + { + ReadyToStartBox.Selected = false; + } + GameMain.Client.SetName(tb.Text); }; }; @@ -1538,6 +1572,13 @@ namespace Barotrauma } } + private void CreateChangesPendingText() + { + if (changesPendingText != null || infoContainer == null) return; + changesPendingText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.065f), infoContainer.Parent.RectTransform, Anchor.BottomCenter, Pivot.TopCenter) { RelativeOffset = new Vector2(0f, -0.065f) }, + TextManager.Get("tabmenu.characterchangespending"), textColor: GUI.Style.Orange, textAlignment: Alignment.Center, style: null) { IgnoreLayoutGroups = true }; + } + private void CreateJobVariantTooltip(JobPrefab jobPrefab, int variant, GUIComponent parentSlot) { jobVariantTooltip = new GUIFrame(new RectTransform(new Point((int)(500 * GUI.Scale), (int)(200 * GUI.Scale)), GUI.Canvas, pivot: Pivot.BottomRight), @@ -1628,19 +1669,19 @@ namespace Barotrauma MissionType = missionType; } - public void UpdateSubList(GUIComponent subList, List submarines) + public void UpdateSubList(GUIComponent subList, List submarines) { if (subList == null) { return; } subList.ClearChildren(); - foreach (Submarine sub in submarines) + foreach (SubmarineInfo sub in submarines) { AddSubmarine(subList, sub); } } - private void AddSubmarine(GUIComponent subList, Submarine sub) + private void AddSubmarine(GUIComponent subList, SubmarineInfo sub) { if (subList is GUIListBox) { @@ -1665,8 +1706,8 @@ namespace Barotrauma CanBeFocused = false }; - var matchingSub = Submarine.SavedSubmarines.FirstOrDefault(s => s.Name == sub.Name && s.MD5Hash?.Hash == sub.MD5Hash?.Hash); - if (matchingSub == null) matchingSub = Submarine.SavedSubmarines.FirstOrDefault(s => s.Name == sub.Name); + var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == sub.Name && s.MD5Hash?.Hash == sub.MD5Hash?.Hash); + if (matchingSub == null) matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == sub.Name); if (matchingSub == null) { @@ -1723,7 +1764,7 @@ namespace Barotrauma { if (!GameMain.Client.ServerSettings.Voting.AllowSubVoting) { - var selectedSub = component.UserData as Submarine; + var selectedSub = component.UserData as SubmarineInfo; if (!selectedSub.RequiredContentPackagesInstalled) { var msgBox = new GUIMessageBox(TextManager.Get("ContentPackageMismatch"), @@ -1748,7 +1789,7 @@ namespace Barotrauma } return false; } - if (component.UserData is Submarine sub) + if (component.UserData is SubmarineInfo sub) { CreateSubPreview(sub); } @@ -1780,7 +1821,7 @@ namespace Barotrauma } GameMain.Client.RequestSelectMode(component.Parent.GetChildIndex(component)); HighlightMode(SelectedModeIndex); - return (presetName.ToLowerInvariant() != "multiplayercampaign"); + return !presetName.Equals("multiplayercampaign", StringComparison.OrdinalIgnoreCase); } return false; } @@ -1812,6 +1853,7 @@ namespace Barotrauma SelectedColor = Color.White * 0.85f, OutlineColor = Color.White * 0.5f, TextColor = Color.White, + SelectedTextColor = Color.Black, UserData = client }; var soundIcon = new GUIImage(new RectTransform(new Point((int)(textBlock.Rect.Height * 0.8f)), textBlock.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(5, 0) }, @@ -1844,28 +1886,28 @@ namespace Barotrauma public void SetPlayerNameAndJobPreference(Client client) { - var playerFrame = (GUITextBlock)PlayerList.Content.FindChild(client); - if (playerFrame == null) { return; } - playerFrame.Text = client.Name; + var PlayerFrame = (GUITextBlock)PlayerList.Content.FindChild(client); + if (PlayerFrame == null) { return; } + PlayerFrame.Text = client.Name; Color color = Color.White; if (JobPrefab.Prefabs.ContainsKey(client.PreferredJob)) { color = JobPrefab.Prefabs[client.PreferredJob].UIColor; } - playerFrame.Color = color * 0.4f; - playerFrame.HoverColor = color * 0.6f; - playerFrame.SelectedColor = color * 0.8f; - playerFrame.OutlineColor = color * 0.5f; - playerFrame.TextColor = color; + PlayerFrame.Color = color * 0.4f; + PlayerFrame.HoverColor = color * 0.6f; + PlayerFrame.SelectedColor = color * 0.8f; + PlayerFrame.OutlineColor = color * 0.5f; + PlayerFrame.TextColor = color; } public void SetPlayerVoiceIconState(Client client, bool muted, bool mutedLocally) { - var playerFrame = PlayerList.Content.FindChild(client); - if (playerFrame == null) { return; } - var soundIcon = playerFrame.FindChild(c => c.UserData is Pair pair && pair.First == "soundicon"); - var soundIconDisabled = playerFrame.FindChild("soundicondisabled"); + var PlayerFrame = PlayerList.Content.FindChild(client); + if (PlayerFrame == null) { return; } + var soundIcon = PlayerFrame.FindChild(c => c.UserData is Pair pair && pair.First == "soundicon"); + var soundIconDisabled = PlayerFrame.FindChild("soundicondisabled"); Pair userdata = soundIcon.UserData as Pair; @@ -1880,9 +1922,9 @@ namespace Barotrauma public void SetPlayerSpeaking(Client client) { - var playerFrame = PlayerList.Content.FindChild(client); - if (playerFrame == null) { return; } - var soundIcon = playerFrame.FindChild(c => c.UserData is Pair pair && pair.First == "soundicon"); + var PlayerFrame = PlayerList.Content.FindChild(client); + if (PlayerFrame == null) { return; } + var soundIcon = PlayerFrame.FindChild(c => c.UserData is Pair pair && pair.First == "soundicon"); Pair userdata = soundIcon.UserData as Pair; userdata.Second = Math.Max(userdata.Second, 0.18f); soundIcon.Visible = true; @@ -1894,18 +1936,18 @@ namespace Barotrauma if (child != null) { playerList.RemoveChild(child); } } - private bool SelectPlayer(Client selectedClient) + public bool SelectPlayer(Client selectedClient) { bool myClient = selectedClient.ID == GameMain.Client.ID; - playerFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas), style: "GUIBackgroundBlocker") + PlayerFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas), style: "GUIBackgroundBlocker") { OnClicked = (btn, userdata) => { if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) ClosePlayerFrame(btn, userdata); return true; } }; - Vector2 frameSize = GameMain.Client.HasPermission(ClientPermissions.ManagePermissions) ? new Vector2(.24f, .5f) : new Vector2(.24f, .24f); + Vector2 frameSize = GameMain.Client.HasPermission(ClientPermissions.ManagePermissions) ? new Vector2(.28f, .5f) : new Vector2(.28f, .24f); - var playerFrameInner = new GUIFrame(new RectTransform(frameSize, playerFrame.RectTransform, Anchor.Center) { MinSize = new Point(550, 0) }); + var playerFrameInner = new GUIFrame(new RectTransform(frameSize, PlayerFrame.RectTransform, Anchor.Center) { MinSize = new Point(550, 0) }); var paddedPlayerFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.88f), playerFrameInner.RectTransform, Anchor.Center)) { Stretch = true, @@ -1938,7 +1980,7 @@ namespace Barotrauma if (GameMain.Client.HasPermission(ClientPermissions.ManagePermissions)) { - playerFrame.UserData = selectedClient; + PlayerFrame.UserData = selectedClient; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), paddedPlayerFrame.RectTransform), TextManager.Get("Rank"), font: GUI.SubHeadingFont); @@ -1964,11 +2006,11 @@ namespace Barotrauma PermissionPreset selectedPreset = (PermissionPreset)userdata; if (selectedPreset != null) { - var client = playerFrame.UserData as Client; + var client = PlayerFrame.UserData as Client; client.SetPermissions(selectedPreset.Permissions, selectedPreset.PermittedCommands); GameMain.Client.UpdateClientPermissions(client); - playerFrame = null; + PlayerFrame = null; SelectPlayer(client); } return true; @@ -2004,7 +2046,7 @@ namespace Barotrauma //reset rank to custom rankDropDown.SelectItem(null); - if (!(playerFrame.UserData is Client client)) { return false; } + if (!(PlayerFrame.UserData is Client client)) { return false; } foreach (GUIComponent child in tickbox.Parent.GetChild().Content.Children) { @@ -2037,7 +2079,7 @@ namespace Barotrauma //reset rank to custom rankDropDown.SelectItem(null); - if (!(playerFrame.UserData is Client client)) { return false; } + if (!(PlayerFrame.UserData is Client client)) { return false; } var thisPermission = (ClientPermissions)tickBox.UserData; if (tickBox.Selected) @@ -2071,7 +2113,7 @@ namespace Barotrauma //reset rank to custom rankDropDown.SelectItem(null); - if (!(playerFrame.UserData is Client client)) { return false; } + if (!(PlayerFrame.UserData is Client client)) { return false; } foreach (GUIComponent child in tickbox.Parent.GetChild().Content.Children) { @@ -2104,7 +2146,7 @@ namespace Barotrauma rankDropDown.SelectItem(null); DebugConsole.Command selectedCommand = tickBox.UserData as DebugConsole.Command; - if (!(playerFrame.UserData is Client client)) { return false; } + if (!(PlayerFrame.UserData is Client client)) { return false; } if (!tickBox.Selected) { @@ -2130,7 +2172,7 @@ namespace Barotrauma { if (GameMain.Client.HasPermission(ClientPermissions.Ban)) { - var banButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonAreaTop.RectTransform), + var banButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaTop.RectTransform), TextManager.Get("Ban")) { UserData = selectedClient @@ -2138,7 +2180,7 @@ namespace Barotrauma banButton.OnClicked = (bt, userdata) => { BanPlayer(selectedClient); return true; }; banButton.OnClicked += ClosePlayerFrame; - var rangebanButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonAreaTop.RectTransform), + var rangebanButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaTop.RectTransform), TextManager.Get("BanRange")) { UserData = selectedClient @@ -2151,7 +2193,7 @@ namespace Barotrauma if (GameMain.Client != null && GameMain.Client.ServerSettings.Voting.AllowVoteKick && selectedClient != null && selectedClient.AllowKicking) { - var kickVoteButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonAreaLower.RectTransform), + var kickVoteButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaLower.RectTransform), TextManager.Get("VoteToKick")) { Enabled = !selectedClient.HasKickVoteFromID(GameMain.Client.ID), @@ -2163,7 +2205,7 @@ namespace Barotrauma if (GameMain.Client.HasPermission(ClientPermissions.Kick) && selectedClient != null && selectedClient.AllowKicking) { - var kickButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonAreaLower.RectTransform), + var kickButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaLower.RectTransform), TextManager.Get("Kick")) { UserData = selectedClient @@ -2172,6 +2214,9 @@ namespace Barotrauma kickButton.OnClicked += ClosePlayerFrame; } + GUITextBlock.AutoScaleAndNormalize( + buttonAreaTop.Children.Select(c => ((GUIButton)c).TextBlock).Concat(buttonAreaLower.Children.Select(c => ((GUIButton)c).TextBlock))); + new GUITickBox(new RectTransform(new Vector2(0.25f, 1.0f), buttonAreaTop.RectTransform, Anchor.TopRight), TextManager.Get("Mute")) { @@ -2181,7 +2226,7 @@ namespace Barotrauma }; } - var closeButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonAreaLower.RectTransform, Anchor.BottomRight), + var closeButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonAreaLower.RectTransform, Anchor.TopRight), TextManager.Get("Close")) { IgnoreLayoutGroups = true, @@ -2202,7 +2247,7 @@ namespace Barotrauma private bool ClosePlayerFrame(GUIButton button, object userData) { - playerFrame = null; + PlayerFrame = null; playerList.Deselect(); return true; } @@ -2229,9 +2274,8 @@ namespace Barotrauma { base.AddToGUIUpdateList(); - playerFrame?.AddToGUIUpdateList(); //CampaignSetupUI?.AddToGUIUpdateList(); - jobInfoFrame?.AddToGUIUpdateList(); + JobInfoFrame?.AddToGUIUpdateList(); HeadSelectionList?.AddToGUIUpdateList(); JobSelectionFrame?.AddToGUIUpdateList(); @@ -2260,7 +2304,7 @@ namespace Barotrauma targetMicStyle = "GUIMicrophoneDisabled"; } - if (targetMicStyle.ToLowerInvariant() != currMicStyle.ToLowerInvariant()) + if (!targetMicStyle.Equals(currMicStyle, StringComparison.OrdinalIgnoreCase)) { GUI.Style.Apply(micIcon, targetMicStyle); } @@ -2528,6 +2572,7 @@ namespace Barotrauma StepValue = 1, BarScrollValue = info.HairIndex, OnMoved = SwitchHair, + OnReleased = SaveHead, BarSize = 1.0f / (float)(hairCount + 1) }; } @@ -2542,6 +2587,7 @@ namespace Barotrauma StepValue = 1, BarScrollValue = info.BeardIndex, OnMoved = SwitchBeard, + OnReleased = SaveHead, BarSize = 1.0f / (float)(beardCount + 1) }; } @@ -2556,6 +2602,7 @@ namespace Barotrauma StepValue = 1, BarScrollValue = info.MoustacheIndex, OnMoved = SwitchMoustache, + OnReleased = SaveHead, BarSize = 1.0f / (float)(moustacheCount + 1) }; } @@ -2570,6 +2617,7 @@ namespace Barotrauma StepValue = 1, BarScrollValue = info.FaceAttachmentIndex, OnMoved = SwitchFaceAttachment, + OnReleased = SaveHead, BarSize = 1.0f / (float)(faceAttachmentCount + 1) }; } @@ -2616,7 +2664,7 @@ namespace Barotrauma GUILayoutGroup row = null; int itemsInRow = 0; - XElement headElement = info.Ragdoll.MainElement.Elements().FirstOrDefault(e => e.GetAttributeString("type", "").ToLowerInvariant() == "head"); + XElement headElement = info.Ragdoll.MainElement.Elements().FirstOrDefault(e => e.GetAttributeString("type", "").Equals("head", StringComparison.OrdinalIgnoreCase)); XElement headSpriteElement = headElement.Element("sprite"); string spritePathWithTags = headSpriteElement.Attribute("texture").Value; @@ -2674,8 +2722,11 @@ namespace Barotrauma private bool SwitchJob(GUIButton button, object obj) { + if (JobList == null) { return false; } + int childIndex = JobList.SelectedIndex; var child = JobList.SelectedComponent; + if (child == null) { return false; } bool moveToNext = obj != null; @@ -2874,7 +2925,7 @@ namespace Barotrauma var textBlock = new GUITextBlock( innerFrame.CountChildren == 0 ? new RectTransform(Vector2.One, parent.RectTransform, Anchor.Center) : - new RectTransform(new Vector2(selectedByPlayer ? 0.65f : 0.95f, 0.3f), parent.RectTransform, Anchor.BottomCenter), + new RectTransform(new Vector2(selectedByPlayer ? 0.55f : 0.95f, 0.3f), parent.RectTransform, Anchor.BottomCenter), jobPrefab.Name, wrap: true, textAlignment: Alignment.BottomCenter) { Padding = Vector4.Zero, @@ -2903,7 +2954,7 @@ namespace Barotrauma info.Head = new CharacterInfo.HeadInfo(id, gender, race); info.ReloadHeadAttachments(); } - StoreHead(); + StoreHead(true); UpdateJobPreferences(JobList); @@ -2911,7 +2962,8 @@ namespace Barotrauma return true; } - + + private bool SaveHead(GUIScrollBar scrollBar, float barScroll) => StoreHead(true); private bool SwitchHair(GUIScrollBar scrollBar, float barScroll) => SwitchAttachment(scrollBar, WearableType.Hair); private bool SwitchBeard(GUIScrollBar scrollBar, float barScroll) => SwitchAttachment(scrollBar, WearableType.Beard); private bool SwitchMoustache(GUIScrollBar scrollBar, float barScroll) => SwitchAttachment(scrollBar, WearableType.Moustache); @@ -2939,14 +2991,15 @@ namespace Barotrauma return false; } info.ReloadHeadAttachments(); - StoreHead(); + StoreHead(false); return true; } - private void StoreHead() + private bool StoreHead(bool save) { var info = GameMain.Client.CharacterInfo; var config = GameMain.Config; + config.CharacterRace = info.Race; config.CharacterGender = info.Gender; config.CharacterHeadIndex = info.HeadSpriteId; @@ -2954,8 +3007,22 @@ namespace Barotrauma config.CharacterBeardIndex = info.BeardIndex; config.CharacterMoustacheIndex = info.MoustacheIndex; config.CharacterFaceAttachmentIndex = info.FaceAttachmentIndex; + + if (save) + { + if (GameMain.GameSession?.GameMode?.IsRunning ?? false) + { + TabMenu.PendingChanges = true; + CreateChangesPendingText(); + } + + GameMain.Config.SaveNewPlayerConfig(); + } + + return true; } + public void SelectMode(int modeIndex) { if (modeIndex < 0 || modeIndex >= modeList.Content.CountChildren) { return; } @@ -3044,7 +3111,7 @@ namespace Barotrauma }*/ } - public void TryDisplayCampaignSubmarine(Submarine submarine) + public void TryDisplayCampaignSubmarine(SubmarineInfo submarine) { string name = submarine?.Name; bool displayed = false; @@ -3053,13 +3120,13 @@ namespace Barotrauma subPreviewContainer.ClearChildren(); foreach (GUIComponent child in subList.Content.Children) { - if (!(child.UserData is Submarine sub)) { continue; } + if (!(child.UserData is SubmarineInfo sub)) { continue; } //just check the name, even though the campaign sub may not be the exact same version //we're selecting the sub just for show, the selection is not actually used for anything if (sub.Name == name) { subList.Select(sub); - if (Submarine.SavedSubmarines.Contains(sub)) + if (SubmarineInfo.SavedSubmarines.Contains(sub)) { CreateSubPreview(sub); displayed = true; @@ -3078,20 +3145,20 @@ namespace Barotrauma { if (!(button.UserData is Pair jobPrefab)) { return false; } - jobInfoFrame = jobPrefab.First.CreateInfoFrame(jobPrefab.Second); - GUIButton closeButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.05f), jobInfoFrame.GetChild(2).GetChild(0).RectTransform, Anchor.BottomRight), + JobInfoFrame = jobPrefab.First.CreateInfoFrame(jobPrefab.Second); + GUIButton closeButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.05f), JobInfoFrame.GetChild(2).GetChild(0).RectTransform, Anchor.BottomRight), TextManager.Get("Close")) { OnClicked = CloseJobInfo }; - jobInfoFrame.OnClicked = (btn, userdata) => { if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) CloseJobInfo(btn, userdata); return true; }; + JobInfoFrame.OnClicked = (btn, userdata) => { if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) CloseJobInfo(btn, userdata); return true; }; return true; } private bool CloseJobInfo(GUIButton button, object obj) { - jobInfoFrame = null; + JobInfoFrame = null; return true; } @@ -3185,8 +3252,14 @@ namespace Barotrauma } GameMain.Client.ForceNameAndJobUpdate(); - if (!GameMain.Config.JobPreferences.SequenceEqual(jobNamePreferences)) + if (!GameMain.Config.AreJobPreferencesEqual(jobNamePreferences)) { + if (GameMain.GameSession?.GameMode?.IsRunning ?? false) + { + TabMenu.PendingChanges = true; + CreateChangesPendingText(); + } + GameMain.Config.JobPreferences = jobNamePreferences; GameMain.Config.SaveNewPlayerConfig(); } @@ -3219,10 +3292,10 @@ namespace Barotrauma { return false; } - - Submarine sub = subList.Content.Children - .FirstOrDefault(c => c.UserData is Submarine s && s.Name == subName && s.MD5Hash?.Hash == md5Hash)? - .UserData as Submarine; + + SubmarineInfo sub = subList.Content.Children + .FirstOrDefault(c => c.UserData is SubmarineInfo s && s.Name == subName && s.MD5Hash?.Hash == md5Hash)? + .UserData as SubmarineInfo; //matching sub found and already selected, all good if (sub != null) @@ -3231,7 +3304,7 @@ namespace Barotrauma { CreateSubPreview(sub); } - if (subList.SelectedData is Submarine selectedSub && selectedSub.MD5Hash?.Hash == md5Hash && System.IO.File.Exists(sub.FilePath)) + if (subList.SelectedData is SubmarineInfo selectedSub && selectedSub.MD5Hash?.Hash == md5Hash && System.IO.File.Exists(sub.FilePath)) { return true; } @@ -3241,8 +3314,8 @@ namespace Barotrauma if (sub == null) { sub = subList.Content.Children - .FirstOrDefault(c => c.UserData is Submarine s && s.Name == subName)? - .UserData as Submarine; + .FirstOrDefault(c => c.UserData is SubmarineInfo s && s.Name == subName)? + .UserData as SubmarineInfo; } //found a sub that at least has the same name, select it @@ -3265,7 +3338,7 @@ namespace Barotrauma FailedSelectedShuttle = null; //hashes match, all good - if (sub.MD5Hash?.Hash == md5Hash && Submarine.SavedSubmarines.Contains(sub)) + if (sub.MD5Hash?.Hash == md5Hash && SubmarineInfo.SavedSubmarines.Contains(sub)) { return true; } @@ -3280,7 +3353,7 @@ namespace Barotrauma FailedSelectedShuttle = new Pair(subName, md5Hash); string errorMsg = ""; - if (sub == null || !Submarine.SavedSubmarines.Contains(sub)) + if (sub == null || !SubmarineInfo.SavedSubmarines.Contains(sub)) { errorMsg = TextManager.GetWithVariable("SubNotFoundError", "[subname]", subName) + " "; } @@ -3322,7 +3395,7 @@ namespace Barotrauma return false; } - private void CreateSubPreview(Submarine sub) + private void CreateSubPreview(SubmarineInfo sub) { subPreviewContainer?.ClearChildren(); sub.CreatePreviewWindow(subPreviewContainer); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs index 922e24e57..08d8d6193 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ParticleEditorScreen.cs @@ -237,7 +237,7 @@ namespace Barotrauma { foreach (XElement element in doc.Root.Elements()) { - if (element.Name.ToString().ToLowerInvariant() != prefab.Name.ToLowerInvariant()) continue; + if (!element.Name.ToString().Equals(prefab.Name, StringComparison.OrdinalIgnoreCase)) { continue; } SerializableProperty.SerializeProperties(prefab, element, true); } } @@ -260,7 +260,7 @@ namespace Barotrauma private void SerializeToClipboard(ParticlePrefab prefab) { #if WINDOWS - if (prefab == null) return; + if (prefab == null) { return; } XmlWriterSettings settings = new XmlWriterSettings { @@ -269,18 +269,40 @@ namespace Barotrauma NewLineOnAttributes = true }; - XElement element = new XElement(prefab.Name); - SerializableProperty.SerializeProperties(prefab, element, true); + XElement originalElement = null; + foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.Particles)) + { + XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); + if (doc == null) { continue; } + + var prefabList = GameMain.ParticleManager.GetPrefabList(); + foreach (ParticlePrefab otherPrefab in prefabList) + { + foreach (XElement subElement in doc.Root.Elements()) + { + if (!subElement.Name.ToString().Equals(prefab.Name, StringComparison.OrdinalIgnoreCase)) { continue; } + SerializableProperty.SerializeProperties(prefab, subElement, true); + originalElement = subElement; + break; + } + } + } + + if (originalElement == null) + { + originalElement = new XElement(prefab.Name); + SerializableProperty.SerializeProperties(prefab, originalElement, true); + } StringBuilder sb = new StringBuilder(); using (var writer = XmlWriter.Create(sb, settings)) { - element.WriteTo(writer); + originalElement.WriteTo(writer); writer.Flush(); } Clipboard.SetText(sb.ToString()); -#endif +#endif } public override void Update(double deltaTime) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs index 7bd320d68..356f0d7cc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen.cs @@ -474,7 +474,7 @@ namespace Barotrauma }; btn.Color *= 0.5f; labelTexts.Add(btn.TextBlock); - + new GUIImage(new RectTransform(new Vector2(0.5f, 0.3f), btn.RectTransform, Anchor.BottomCenter, scaleBasis: ScaleBasis.BothHeight), style: "GUIButtonVerticalArrow", scaleToFit: true) { CanBeFocused = false, @@ -567,7 +567,18 @@ namespace Barotrauma var directJoinButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform), TextManager.Get("serverlistdirectjoin")) { - OnClicked = (btn, userdata) => { ShowDirectJoinPrompt(); return true; } + OnClicked = (btn, userdata) => + { + if (string.IsNullOrWhiteSpace(ClientNameBox.Text)) + { + ClientNameBox.Flash(); + ClientNameBox.Select(); + GUI.PlayUISound(GUISoundType.PickItemFail); + return false; + } + ShowDirectJoinPrompt(); + return true; + } }; joinButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform), @@ -909,6 +920,12 @@ namespace Barotrauma Steamworks.SteamMatchmaking.ResetActions(); + if (GameMain.Client != null) + { + GameMain.Client.Disconnect(); + GameMain.Client = null; + } + RefreshServers(); } @@ -965,7 +982,7 @@ namespace Barotrauma child.Visible = serverInfo.OwnerVerified && - serverInfo.ServerName.ToLowerInvariant().Contains(searchBox.Text.ToLowerInvariant()) && + serverInfo.ServerName.Contains(searchBox.Text, StringComparison.OrdinalIgnoreCase) && (!filterSameVersion.Selected || (remoteVersion != null && NetworkMember.IsCompatible(remoteVersion, GameMain.Version))) && (!filterPassword.Selected || !serverInfo.HasPassword) && (!filterIncompatible.Selected || !incompatible) && @@ -996,7 +1013,7 @@ namespace Barotrauma foreach (GUITickBox tickBox in gameModeTickBoxes) { var gameMode = (string)tickBox.UserData; - if (!tickBox.Selected && (serverInfo.GameMode == gameMode.ToLowerInvariant() || serverInfo.GameMode == gameMode)) + if (!tickBox.Selected && serverInfo.GameMode.Equals(gameMode, StringComparison.OrdinalIgnoreCase)) { child.Visible = false; break; @@ -1304,6 +1321,8 @@ namespace Barotrauma { #if DEBUG DebugConsole.ThrowError($"Failed to parse a Steam friend's connect command ({connectCommand})", e); +#else + DebugConsole.Log($"Failed to parse a Steam friend's connect command ({connectCommand})\n" + e.StackTrace); #endif info.ConnectName = null; info.ConnectEndpoint = null; @@ -1512,7 +1531,7 @@ namespace Barotrauma { serverList.ClearChildren(); - if (masterServerData.Substring(0, 5).ToLowerInvariant() == "error") + if (masterServerData.Substring(0, 5).Equals("error", StringComparison.OrdinalIgnoreCase)) { DebugConsole.ThrowError("Error while connecting to master server (" + masterServerData + ")!"); return; @@ -1895,6 +1914,8 @@ namespace Barotrauma if (string.IsNullOrWhiteSpace(ClientNameBox.Text)) { ClientNameBox.Flash(); + ClientNameBox.Select(); + GUI.PlayUISound(GUISoundType.PickItemFail); return false; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs index 3fc91dcf5..6fda6eb90 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs @@ -196,8 +196,9 @@ namespace Barotrauma Stretch = true, UserData = "filterarea" }; - filterTexturesLabel = new GUITextBlock(new RectTransform(Vector2.One, filterArea.RectTransform), TextManager.Get("serverlog.filter"), font: GUI.Font) { IgnoreLayoutGroups = true }; ; + filterTexturesLabel = new GUITextBlock(new RectTransform(Vector2.One, filterArea.RectTransform), TextManager.Get("serverlog.filter"), font: GUI.Font, textAlignment: Alignment.CenterLeft) { IgnoreLayoutGroups = true }; ; filterTexturesBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1.0f), filterArea.RectTransform), font: GUI.Font, createClearButton: true); + filterArea.RectTransform.MinSize = filterTexturesBox.RectTransform.MinSize; filterTexturesBox.OnTextChanged += (textBox, text) => { FilterTextures(text); return true; }; textureList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), paddedLeftPanel.RectTransform)) @@ -240,8 +241,9 @@ namespace Barotrauma Stretch = true, UserData = "filterarea" }; - filterSpritesLabel = new GUITextBlock(new RectTransform(Vector2.One, filterArea.RectTransform), TextManager.Get("serverlog.filter"), font: GUI.Font) { IgnoreLayoutGroups = true }; + filterSpritesLabel = new GUITextBlock(new RectTransform(Vector2.One, filterArea.RectTransform), TextManager.Get("serverlog.filter"), font: GUI.Font, textAlignment: Alignment.CenterLeft) { IgnoreLayoutGroups = true }; filterSpritesBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1.0f), filterArea.RectTransform), font: GUI.Font, createClearButton: true); + filterArea.RectTransform.MinSize = filterSpritesBox.RectTransform.MinSize; filterSpritesBox.OnTextChanged += (textBox, text) => { FilterSprites(text); return true; }; spriteList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), paddedRightPanel.RectTransform)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs index 59ef3ab77..1d7663c4b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SteamWorkshopScreen.cs @@ -26,10 +26,24 @@ namespace Barotrauma //listbox that shows the files included in the item being created private GUIListBox createItemFileList; + private FileSystemWatcher createItemWatcher; + private readonly List tabButtons = new List(); - private readonly HashSet pendingPreviewImageDownloads = new HashSet(); - private readonly Dictionary itemPreviewSprites = new Dictionary(); + private class PendingPreviewImageDownload + { + /// + /// Was the image downloaded + /// + public bool Downloaded = false; + + /// + /// How many tasks are looking to create a preview image based on this download + /// + public int PendingLoads = 1; + } + private readonly Dictionary pendingPreviewImageDownloads = new Dictionary(); + private Dictionary itemPreviewSprites = new Dictionary(); private enum Tab { @@ -54,6 +68,8 @@ namespace Barotrauma { GameMain.Instance.OnResolutionChanged += CreateUI; CreateUI(); + + Steamworks.SteamUGC.GlobalOnItemInstalled += OnItemInstalled; } private void CreateUI() @@ -199,7 +215,7 @@ namespace Barotrauma { if (GUI.MouseOn is GUIButton || GUI.MouseOn?.Parent is GUIButton) { return false; } publishedItemList.Deselect(); - if (userdata is Submarine sub) + if (userdata is SubmarineInfo sub) { CreateWorkshopItem(sub); } @@ -215,6 +231,8 @@ namespace Barotrauma createItemFrame = new GUIFrame(new RectTransform(new Vector2(0.58f, 1.0f), tabs[(int)Tab.Publish].RectTransform, Anchor.TopRight), style: null); SelectTab(Tab.Mods); + + subscribedCoroutine = CoroutineManager.StartCoroutine(PollSubscribedItems()); } public override void Select() @@ -230,11 +248,37 @@ namespace Barotrauma SelectTab(Tab.Mods); } + private void OnItemInstalled(ulong itemId) + { + RefreshSubscribedItems(); + } + + CoroutineHandle subscribedCoroutine; + + private IEnumerable PollSubscribedItems() + { + if (!SteamManager.IsInitialized) { yield return CoroutineStatus.Success; } + + uint numSubscribed = 0; + while (true) + { + while (CoroutineManager.IsCoroutineRunning("Load")) { yield return new WaitForSeconds(1.0f); } + uint newNumSubscribed = Steamworks.SteamUGC.NumSubscribedItems; + if (newNumSubscribed != numSubscribed) + { + RefreshSubscribedItems(); + numSubscribed = newNumSubscribed; + } + + yield return new WaitForSeconds(1.0f); + } + } + private void SelectTab(Tab tab) { for (int i = 0; i < tabs.Length; i++) { - tabButtons[i].Selected = tabs[i].Visible = i == (int)tab; + tabButtons[i].Selected = tabs[i].Visible = i == (int)tab; } if (createItemFrame.CountChildren == 0) @@ -246,6 +290,7 @@ namespace Barotrauma }; } + createItemWatcher?.Dispose(); createItemWatcher = null; if (Screen.Selected == this) { switch (tab) @@ -272,6 +317,25 @@ namespace Barotrauma GameMain.SteamWorkshopScreen.Select(); } + public IEnumerable RefreshDownloadState() + { + bool isDownloading = true; + while (true) + { + SteamManager.GetSubscribedWorkshopItems((items) => + { + isDownloading = items.Any(it => it.IsDownloading || it.IsDownloadPending); + + GameMain.MainMenuScreen.SetDownloadingModsNotification(isDownloading); + }); + + if (!isDownloading) { break; } + + yield return new WaitForSeconds(0.5f); + } + yield return CoroutineStatus.Success; + } + private void RefreshSubscribedItems() { SteamManager.GetSubscribedWorkshopItems((items) => @@ -279,6 +343,8 @@ namespace Barotrauma //filter out the items published by the player (they're shown in the publish tab) var mySteamID = SteamManager.GetSteamID(); OnItemsReceived(GetVisibleItems(items.Where(it => it.Owner.Id != mySteamID)), subscribedItemList); + + GameMain.MainMenuScreen.SetDownloadingModsNotification(items.Any(it => it.IsDownloading || it.IsDownloadPending)); }); } @@ -312,7 +378,7 @@ namespace Barotrauma { CanBeFocused = false }; - foreach (Submarine sub in Submarine.SavedSubmarines) + foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) { if (sub.HasTag(SubmarineTag.HideInMenus)) { continue; } string subPath = Path.GetFullPath(sub.FilePath); @@ -348,6 +414,7 @@ namespace Barotrauma foreach (ContentPackage contentPackage in ContentPackage.List) { if (!string.IsNullOrEmpty(contentPackage.SteamWorkshopUrl) || contentPackage.HideInWorkshopMenu) { continue; } + if (contentPackage == GameMain.VanillaContent) { continue; } //don't list content packages that only define one sub (they're visible in the "Submarines" section) if (contentPackage.Files.Count == 1 && contentPackage.Files[0].Type == ContentType.Submarine) { continue; } CreateMyItemFrame(contentPackage, myItemList); @@ -414,7 +481,7 @@ namespace Barotrauma CanBeFocused = false }; } - else + else if (Screen.Selected == this) { new GUIImage(new RectTransform(new Point(iconSize), innerFrame.RectTransform), SteamManager.DefaultPreviewImage, scaleToFit: true) { @@ -430,16 +497,20 @@ namespace Barotrauma bool isNewImage; lock (pendingPreviewImageDownloads) { - isNewImage = !pendingPreviewImageDownloads.Contains(item?.PreviewImageUrl); - if (isNewImage) { pendingPreviewImageDownloads.Add(item?.PreviewImageUrl); } + isNewImage = !pendingPreviewImageDownloads.ContainsKey(item.Value.Id); + if (isNewImage) + { + if (File.Exists(imagePreviewPath)) + { + File.Delete(imagePreviewPath); + } + + pendingPreviewImageDownloads.Add(item.Value.Id, new PendingPreviewImageDownload()); + } } if (isNewImage) { - if (File.Exists(imagePreviewPath)) - { - File.Delete(imagePreviewPath); - } Directory.CreateDirectory(SteamManager.WorkshopItemPreviewImageFolder); Uri baseAddress = new Uri(item?.PreviewImageUrl); @@ -450,16 +521,23 @@ namespace Barotrauma var request = new RestRequest(fileName, Method.GET); client.ExecuteAsync(request, response => { - lock (pendingPreviewImageDownloads) - { - pendingPreviewImageDownloads.Remove(item?.PreviewImageUrl); - } - OnPreviewImageDownloaded(response, imagePreviewPath); - CoroutineManager.StartCoroutine(WaitForItemPreviewDownloaded(item, listBox, imagePreviewPath)); + OnPreviewImageDownloaded(response, imagePreviewPath, + () => + { + lock (pendingPreviewImageDownloads) + { + pendingPreviewImageDownloads[item.Value.Id].Downloaded = true; + } + CoroutineManager.StartCoroutine(WaitForItemPreviewDownloaded(item, listBox, imagePreviewPath)); + }); }); } else { + lock (pendingPreviewImageDownloads) + { + pendingPreviewImageDownloads[item.Value.Id].PendingLoads++; + } CoroutineManager.StartCoroutine(WaitForItemPreviewDownloaded(item, listBox, imagePreviewPath)); } } @@ -468,7 +546,7 @@ namespace Barotrauma { lock (pendingPreviewImageDownloads) { - pendingPreviewImageDownloads.Remove(item?.PreviewImageUrl); + pendingPreviewImageDownloads.Remove(item.Value.Id); } DebugConsole.ThrowError("Downloading the preview image of the Workshop item \"" + item?.Title + "\" failed.", e); } @@ -488,10 +566,11 @@ namespace Barotrauma CanBeFocused = false }; - if ((item?.IsSubscribed ?? false) && (item?.IsInstalled ?? false)) + if ((item?.IsSubscribed ?? false) && (item?.IsInstalled ?? false) && Directory.Exists(item?.Directory)) { - GUITickBox enabledTickBox = null; - try + bool installed = SteamManager.CheckWorkshopItemEnabled(item); + + if (!installed) { bool? compatible = SteamManager.CheckWorkshopItemCompatibility(item); if (compatible.HasValue && !compatible.Value) @@ -504,63 +583,29 @@ namespace Barotrauma } else { - enabledTickBox = new GUITickBox(new RectTransform(new Point(32, 32), rightColumn.RectTransform), null) + installed = SteamManager.EnableWorkShopItem(item, true, out string errorMsg, Selected == this); + if (!installed) { - ToolTip = TextManager.Get("WorkshopItemEnabled"), - UserData = item, - }; - enabledTickBox.Selected = SteamManager.CheckWorkshopItemEnabled(item); - enabledTickBox.OnSelected = ToggleItemEnabled; - } - } - catch (Exception e) - { - if (enabledTickBox != null) { enabledTickBox.Enabled = false; } - itemFrame.ToolTip = e.Message; - itemFrame.Color = GUI.Style.Red; - itemFrame.HoverColor = GUI.Style.Red; - itemFrame.SelectedColor = GUI.Style.Red; - titleText.TextColor = GUI.Style.Red; - - if (item?.IsSubscribed ?? false) - { - new GUIButton(new RectTransform(new Vector2(0.5f, 0.5f), rightColumn.RectTransform), TextManager.Get("WorkshopItemUnsubscribe")) - { - UserData = item, - OnClicked = (btn, userdata) => - { - item?.Unsubscribe(); - subscribedItemList.RemoveChild(subscribedItemList.Content.GetChildByUserData(item)); - return true; - } - }; - } - } - - if (listBox != publishedItemList && SteamManager.CheckWorkshopItemEnabled(item) && !SteamManager.CheckWorkshopItemUpToDate(item)) - { - new GUIButton(new RectTransform(new Vector2(0.4f, 0.5f), rightColumn.RectTransform, Anchor.BottomLeft), text: TextManager.Get("WorkshopItemUpdate")) - { - UserData = "updatebutton", - Font = GUI.SmallFont, - OnClicked = (btn, userdata) => - { - if (SteamManager.UpdateWorkshopItem(item, out string errorMsg)) - { - new GUIMessageBox("", TextManager.GetWithVariable("WorkshopItemUpdated", "[itemname]", item?.Title)); - } - else - { - DebugConsole.ThrowError(errorMsg); - new GUIMessageBox( - TextManager.Get("Error"), - TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { item?.Title, errorMsg })); - } - btn.Enabled = false; - btn.Visible = false; - return true; + DebugConsole.NewMessage(errorMsg, Color.Red); + titleText.TextColor = Color.Red; + titleText.ToolTip = itemFrame.ToolTip = TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { TextManager.EnsureUTF8(item?.Title), errorMsg }); } - }; + } + } + + if (installed) + { + bool upToDate = SteamManager.CheckWorkshopItemUpToDate(item); + + if (!upToDate) + { + if (!SteamManager.UpdateWorkshopItem(item, out string errorMsg)) + { + DebugConsole.NewMessage(errorMsg, Color.Red); + titleText.TextColor = Color.Red; + titleText.ToolTip = itemFrame.ToolTip = TextManager.GetWithVariables("WorkshopItemUpdateFailed", new string[2] { "[itemname]", "[errormessage]" }, new string[2] { TextManager.EnsureUTF8(item?.Title), errorMsg }); + } + } } } @@ -568,7 +613,11 @@ namespace Barotrauma { new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.5f), rightColumn.RectTransform), TextManager.Get("WorkshopItemDownloading")); } - else + else if (item?.IsDownloadPending ?? false) + { + new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.5f), rightColumn.RectTransform), TextManager.Get("WorkshopItemDownloadPending")); + } + else if (!(item?.IsSubscribed ?? false)) { var downloadBtn = new GUIButton(new RectTransform(new Point((int)(32 * GUI.Scale)), rightColumn.RectTransform), "", style: "GUIPlusButton") { @@ -579,10 +628,66 @@ namespace Barotrauma downloadBtn.OnClicked = (btn, userdata) => { DownloadItem(itemFrame, downloadBtn, item); return true; }; } + if ((item?.IsSubscribed ?? false) && listBox == subscribedItemList) + { + var reinstallBtn = new GUIButton(new RectTransform(new Point((int)(32 * GUI.Scale)), rightColumn.RectTransform), "", style: "GUIReloadButton") + { + ToolTip = TextManager.Get("WorkshopItemReinstall"), + ForceUpperCase = true, + UserData = "reinstall" + }; + reinstallBtn.OnClicked = (btn, userdata) => + { + var elem = subscribedItemList.Content.GetChildByUserData(item); + try + { + bool reselect = GameMain.Config.SelectedContentPackages.Any(cp => !string.IsNullOrWhiteSpace(cp.SteamWorkshopUrl) && cp.SteamWorkshopUrl == item?.Url); + if (!SteamManager.DisableWorkShopItem(item, false, out string errorMsg) || + !SteamManager.EnableWorkShopItem(item, true, out errorMsg, reselect, true)) + { + DebugConsole.ThrowError($"Failed to reinstall \"{item?.Title}\": {errorMsg}", null, true); + elem.Flash(GUI.Style.Red); + } + } + catch (Exception e) + { + DebugConsole.ThrowError($"Failed to reinstall \"{item?.Title}\"", e, true); + elem.Flash(GUI.Style.Red); + } + return true; + }; + var unsubBtn = new GUIButton(new RectTransform(new Point((int)(32 * GUI.Scale)), rightColumn.RectTransform), "", style: "GUIMinusButton") + { + ToolTip = TextManager.Get("WorkshopItemUnsubscribe"), + ForceUpperCase = true, + UserData = "unsubscribe" + }; + unsubBtn.OnClicked = (btn, userdata) => + { + SteamManager.DisableWorkShopItem(item, true, out _); + item?.Unsubscribe(); + subscribedItemList.RemoveChild(subscribedItemList.Content.GetChildByUserData(item)); + return true; + }; + } + innerFrame.Recalculate(); listBox.RecalculateChildren(); } + public void SetReinstallButtonStatus(Steamworks.Ugc.Item? item, bool enabled, Color? flashColor) + { + var child = subscribedItemList.Content.FindChild((component) => { return (component.UserData is Steamworks.Ugc.Item?) && (component.UserData as Steamworks.Ugc.Item?)?.Id == item?.Id; }); + if (child != null) + { + var reinstallBtn = child.FindChild("reinstall", true); + if (reinstallBtn != null) { reinstallBtn.Enabled = enabled; } + var unsubBtn = child.FindChild("unsubscribe", true); + if (unsubBtn != null) { unsubBtn.Enabled = enabled; } + if (flashColor.HasValue) { child.Flash(flashColor); } + } + } + private void RemoveItemFromLists(ulong itemID) { RemoveItemFromList(publishedItemList); @@ -596,7 +701,7 @@ namespace Barotrauma } } - private void CreateMyItemFrame(Submarine submarine, GUIListBox listBox) + private void CreateMyItemFrame(SubmarineInfo submarine, GUIListBox listBox) { var itemFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), listBox.Content.RectTransform, minSize: new Point(0, 80)), style: "ListBoxElement") @@ -629,21 +734,27 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), innerFrame.RectTransform), contentPackage.Name, textAlignment: Alignment.CenterLeft); } - private void OnPreviewImageDownloaded(IRestResponse response, string previewImagePath) + private void OnPreviewImageDownloaded(IRestResponse response, string previewImagePath, Action action) { if (response.ResponseStatus == ResponseStatus.Completed) { - try - { - File.WriteAllBytes(previewImagePath, response.RawBytes); - } - catch (Exception e) - { - string errorMsg = "Failed to save workshop item preview image to \"" + previewImagePath + "\"."; - GameAnalyticsManager.AddErrorEventOnce("SteamWorkshopScreen.OnItemPreviewDownloaded:WriteAllBytesFailed" + previewImagePath, - GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg + "\n" + e.Message); - return; - } + TaskPool.Add(WritePreviewImageAsync(response, previewImagePath), (task) => { action?.Invoke(); }); + } + } + + private async Task WritePreviewImageAsync(IRestResponse response, string previewImagePath) + { + await Task.Yield(); + try + { + File.WriteAllBytes(previewImagePath, response.RawBytes); + } + catch (Exception e) + { + string errorMsg = "Failed to save workshop item preview image to \"" + previewImagePath + "\"."; + GameAnalyticsManager.AddErrorEventOnce("SteamWorkshopScreen.OnItemPreviewDownloaded:WriteAllBytesFailed" + previewImagePath, + GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg + "\n" + e.Message); + return; } } @@ -653,47 +764,67 @@ namespace Barotrauma { lock (pendingPreviewImageDownloads) { - if (!pendingPreviewImageDownloads.Contains(item?.PreviewImageUrl)) { break; } + if (pendingPreviewImageDownloads[item.Value.Id].Downloaded){ break; } } - yield return CoroutineStatus.Running; + yield return new WaitForSeconds(0.2f); } if (File.Exists(previewImagePath)) { - Sprite newSprite; - if (itemPreviewSprites.ContainsKey(item?.PreviewImageUrl)) + TaskPool.Add(LoadPreviewImageAsync(item?.PreviewImageUrl, previewImagePath), + new Tuple(item, listBox), + (task, tuple) => { - newSprite = itemPreviewSprites[item?.PreviewImageUrl]; - } - else - { - newSprite = new Sprite(previewImagePath, sourceRectangle: null); - itemPreviewSprites.Add(item?.PreviewImageUrl, newSprite); - } + (var it, var lb) = tuple; + var previewImage = lb.Content.FindChild(item)?.GetChildByUserData("previewimage") as GUIImage; + if (previewImage != null) + { + previewImage.Sprite = task.Result; + } + else + { + CreateWorkshopItemFrame(it, lb); + } - if (listBox.Content.FindChild(item)?.GetChildByUserData("previewimage") is GUIImage previewImage) - { - previewImage.Sprite = newSprite; - } - else - { - CreateWorkshopItemFrame(item, listBox); - } + if (modsPreviewFrame.FindChild(it) != null) + { + ShowItemPreview(it, modsPreviewFrame); + } + if (browsePreviewFrame.FindChild(item) != null) + { + ShowItemPreview(it, browsePreviewFrame); + } - if (modsPreviewFrame.FindChild(item) != null) - { - ShowItemPreview(item, modsPreviewFrame); - } - if (browsePreviewFrame.FindChild(item) != null) - { - ShowItemPreview(item, browsePreviewFrame); - } + lock (pendingPreviewImageDownloads) + { + pendingPreviewImageDownloads[it.Value.Id].PendingLoads--; + if (pendingPreviewImageDownloads[it.Value.Id].PendingLoads <= 0) { pendingPreviewImageDownloads.Remove(it.Value.Id); } + } + }); } yield return CoroutineStatus.Success; } + private async Task LoadPreviewImageAsync(string previewImageUrl, string previewImagePath) + { + await Task.Yield(); + lock (itemPreviewSprites) + { + if (itemPreviewSprites.ContainsKey(previewImageUrl)) + { + return itemPreviewSprites[previewImageUrl]; + } + else + { + Sprite newSprite = new Sprite(previewImagePath, sourceRectangle: null); + itemPreviewSprites.Add(previewImageUrl, newSprite); + return newSprite; + } + } + } + private bool DownloadItem(GUIComponent frame, GUIButton downloadButton, Steamworks.Ugc.Item? item) { if (item == null) { return false; } @@ -721,49 +852,6 @@ namespace Barotrauma return true; } - private bool ToggleItemEnabled(GUITickBox tickBox) - { - if (!(tickBox.UserData is Steamworks.Ugc.Item?)) { return false; } - - var item = tickBox.UserData as Steamworks.Ugc.Item?; - if (item == null) { return false; } - - //currently editing the item, don't allow enabling/disabling it - if (itemEditor?.FileId == item?.Id) { tickBox.Selected = true; return false; } - - var updateButton = tickBox.Parent.FindChild("updatebutton"); - - string errorMsg; - if (tickBox.Selected) - { - if (!SteamManager.EnableWorkShopItem(item, false, out errorMsg)) - { - tickBox.Visible = false; - tickBox.Selected = false; - if (tickBox.Parent.GetChildByUserData("titletext") is GUITextBlock titleText) { titleText.TextColor = GUI.Style.Red; } - } - } - else - { - if (!SteamManager.DisableWorkShopItem(item, false, out errorMsg)) - { - tickBox.Enabled = false; - } - GameMain.Config.EnsureCoreContentPackageSelected(); - } - if (updateButton != null) - { - //cannot update if enabling/disabling the item failed or if the item is not enabled - updateButton.Enabled = tickBox.Enabled && tickBox.Selected; - } - if (!string.IsNullOrEmpty(errorMsg)) - { - new GUIMessageBox(TextManager.Get("Error"), errorMsg); - } - - return true; - } - private void ShowItemPreview(Steamworks.Ugc.Item? item, GUIFrame itemPreviewFrame) { itemPreviewFrame.ClearChildren(); @@ -920,9 +1008,9 @@ namespace Barotrauma }; } - private void CreateWorkshopItem(Submarine sub) + private void CreateWorkshopItem(SubmarineInfo sub) { - string destinationFolder = Path.Combine("Mods", sub.Name); + string destinationFolder = Path.Combine("Mods", sub.Name.Trim()); itemContentPackage = ContentPackage.CreatePackage(sub.Name, Path.Combine(destinationFolder, SteamManager.MetadataFileName), corePackage: false); SteamManager.CreateWorkshopItemStaging(itemContentPackage, out itemEditor); @@ -940,8 +1028,8 @@ namespace Barotrauma itemContentPackage.AddFile(sub.FilePath, ContentType.Submarine); itemContentPackage.Name = sub.Name; itemContentPackage.Save(itemContentPackage.Path); - ContentPackage.List.Add(itemContentPackage); - GameMain.Config.SelectContentPackage(itemContentPackage); + //ContentPackage.List.Add(itemContentPackage); + //GameMain.Config.SelectContentPackage(itemContentPackage); itemEditor = itemEditor?.WithTitle(sub.Name).WithTag("Submarine").WithDescription(sub.Description); @@ -1097,7 +1185,7 @@ namespace Barotrauma var tagBtn = new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), tagHolder.Content.RectTransform, anchor: Anchor.CenterLeft), tag.CapitaliseFirstInvariant(), style: "GUIButtonRound"); tagBtn.TextBlock.AutoScaleHorizontal = true; - tagBtn.Selected = itemEditor?.Tags?.Any(t => t.ToLowerInvariant() == tag) ?? false; + tagBtn.Selected = itemEditor?.Tags?.Any(t => t.Equals(tag, StringComparison.OrdinalIgnoreCase)) ?? false; tagBtn.OnClicked = (btn, userdata) => { @@ -1108,7 +1196,7 @@ namespace Barotrauma } else { - itemEditor?.Tags?.RemoveAll(t => t.ToLowerInvariant() == tagBtn.Text.ToLowerInvariant()); + itemEditor?.Tags?.RemoveAll(t => t.Equals(tagBtn.Text, StringComparison.OrdinalIgnoreCase)); tagBtn.Selected = false; } return true; @@ -1201,6 +1289,16 @@ namespace Barotrauma OnClicked = (btn, userdata) => { ToolBox.OpenFileWithShell(Path.GetFullPath(Path.GetDirectoryName(itemContentPackage.Path))); return true; } }; createItemFileList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.35f), createItemContent.RectTransform)); + createItemWatcher?.Dispose(); + createItemWatcher = new FileSystemWatcher(Path.GetDirectoryName(itemContentPackage.Path)) + { + Filter = "*", + NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName + }; + createItemWatcher.Created += OnFileSystemChanges; + createItemWatcher.Deleted += OnFileSystemChanges; + createItemWatcher.Renamed += OnFileSystemChanges; + createItemWatcher.EnableRaisingEvents = true; RefreshCreateItemFileList(); var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), createItemContent.RectTransform), isHorizontal: true) @@ -1447,23 +1545,54 @@ namespace Barotrauma { destinationPath = Path.Combine(modFolder, filePathRelativeToModFolder); } - itemContentPackage.AddFile(destinationPath, ContentType.None); } - itemContentPackage.Save(itemContentPackage.Path); RefreshCreateItemFileList(); } - + + volatile bool refreshFileList = false; + + private void OnFileSystemChanges(object sender, FileSystemEventArgs e) + { + refreshFileList = true; + } + private void RefreshCreateItemFileList() { createItemFileList.ClearChildren(); if (itemContentPackage == null) return; var contentTypes = Enum.GetValues(typeof(ContentType)); - - foreach (ContentFile contentFile in itemContentPackage.Files) + + List files = itemContentPackage.Files.ToList(); + + foreach (ContentFile contentFile in files) + { + bool fileExists = File.Exists(contentFile.Path); + + if (!fileExists) { itemContentPackage.Files.Remove(contentFile); continue; } + } + + List allFiles = Directory.GetFiles(Path.GetDirectoryName(itemContentPackage.Path), "*", SearchOption.AllDirectories) + .Select(f => new ContentFile(f, ContentType.None)) + .Where(file => Path.GetFileName(file.Path) != SteamManager.MetadataFileName && + Path.GetFileName(file.Path) != SteamManager.PreviewImageName) + .ToList(); + for (int i=0;i string.Equals(Path.GetFullPath(f.Path).CleanUpPath(), + Path.GetFullPath(file.Path).CleanUpPath(), + StringComparison.InvariantCultureIgnoreCase)); + if (otherFile != null) + { + //replace the generated ContentFile object with the one that's present in the + //content package to determine which tickboxes should already be checked + allFiles[i] = otherFile; + } + } + + foreach (ContentFile contentFile in allFiles) { bool illegalPath = !ContentPackage.IsModFilePathAllowed(contentFile); - //string pathInStagingFolder = Path.Combine(SteamManager.WorkshopItemStagingFolder, contentFile.Path); - //bool fileInStagingFolder = File.Exists(pathInStagingFolder); bool fileExists = File.Exists(contentFile.Path); var fileFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.12f), createItemFileList.Content.RectTransform) { MinSize = new Point(0, 20) }, @@ -1479,11 +1608,25 @@ namespace Barotrauma RelativeSpacing = 0.05f }; - var tickBox = new GUITickBox(new RectTransform(new Vector2(0.1f, 0.8f), content.RectTransform), "") + var tickBox = new GUITickBox(new RectTransform(Vector2.One, content.RectTransform, scaleBasis: ScaleBasis.BothHeight), "") { - Selected = fileExists && !illegalPath, - Enabled = false, - ToolTip = TextManager.Get(illegalPath ? "WorkshopItemFileNotIncluded" : "WorkshopItemFileIncluded") + Selected = itemContentPackage.Files.Contains(contentFile), + UserData = contentFile + }; + + tickBox.OnSelected = (tb) => + { + ContentFile f = tb.UserData as ContentFile; + if (tb.Selected) + { + if (!itemContentPackage.Files.Contains(f)) { itemContentPackage.Files.Add(f); } + } + else + { + if (itemContentPackage.Files.Contains(f)) { itemContentPackage.Files.Remove(f); } + } + + return true; }; var nameText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), content.RectTransform, Anchor.CenterLeft), contentFile.Path, font: GUI.SmallFont) @@ -1523,10 +1666,29 @@ namespace Barotrauma { OnClicked = (btn, userdata) => { - itemContentPackage.RemoveFile(contentFile); - itemContentPackage.Save(itemContentPackage.Path); - RefreshCreateItemFileList(); - RefreshMyItemList(); + var msgBox = new GUIMessageBox(TextManager.Get("ConfirmFileDeletionHeader"), + TextManager.GetWithVariable("ConfirmFileDeletion", "[file]", contentFile.Path), + new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }) + { + UserData = "verificationprompt" + }; + msgBox.Buttons[0].OnClicked = (applyButton, obj) => + { + try + { + File.Delete(contentFile.Path); + if (contentFile.Type == ContentType.Submarine) { SubmarineInfo.RefreshSavedSub(contentFile.Path); } + } + catch (Exception e) + { + DebugConsole.ThrowError($"Failed to delete \"${contentFile.Path}\".", e); + } + //RefreshCreateItemFileList(); + RefreshMyItemList(); + return true; + }; + msgBox.Buttons[0].OnClicked += msgBox.Close; + msgBox.Buttons[1].OnClicked = msgBox.Close; return true; } }; @@ -1536,6 +1698,8 @@ namespace Barotrauma new Point(0, (int)(content.RectTransform.Children.Max(c => c.MinSize.Y) / content.RectTransform.RelativeSize.Y)); nameText.Text = ToolBox.LimitString(nameText.Text, nameText.Font, maxWidth: nameText.Rect.Width); } + + itemContentPackage.Save(itemContentPackage.Path); } private void PublishWorkshopItem() @@ -1640,6 +1804,11 @@ namespace Barotrauma public override void Update(double deltaTime) { + if (refreshFileList) + { + RefreshCreateItemFileList(); + refreshFileList = false; + } } #endregion diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 4de176323..34b881c19 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -6,13 +6,17 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using System.Xml.Linq; +using Microsoft.Xna.Framework.Input; + +// ReSharper disable AccessToModifiedClosure, PossibleLossOfFraction, RedundantLambdaParameterType, UnusedVariable namespace Barotrauma { class SubEditorScreen : Screen { - private static readonly string[] crewExperienceLevels = new string[] + private static readonly string[] crewExperienceLevels = { "CrewExperienceLow", "CrewExperienceMid", @@ -22,24 +26,32 @@ namespace Barotrauma public enum Mode { Default, - Character, Wiring } + + public static Vector2 MouseDragStart = Vector2.Zero; private readonly Point defaultPreviewImageSize = new Point(640, 368); private readonly Camera cam; + private Vector2 camTargetFocus = Vector2.Zero; + + private SubmarineInfo backedUpSubInfo; private Point screenResolution; private bool lightingEnabled; + private bool wasSelectedBefore; + public GUIComponent TopPanel; private GUIComponent showEntitiesPanel, entityCountPanel; - private List showEntitiesTickBoxes = new List(); + private readonly List showEntitiesTickBoxes = new List(); private GUITextBlock subNameLabel; + private bool showThalamus = true; + private bool entityMenuOpen = true; private float entityMenuOpenState = 1.0f; public GUIComponent EntityMenu; @@ -47,7 +59,9 @@ namespace Barotrauma private GUIListBox entityList; private GUIButton toggleEntityMenuButton; - private GUITickBox defaultModeTickBox, wiringModeTickBox, characterModeTickBox; + public GUIButton ToggleEntityMenuButton => toggleEntityMenuButton; + + private GUITickBox defaultModeTickBox, wiringModeTickBox; private GUIComponent loadFrame, saveFrame; @@ -70,12 +84,28 @@ namespace Barotrauma //a Character used for picking up and manipulating items private Character dummyCharacter; + + /// + /// Prefab used for dragging from the item catalog into inventories + /// + /// + public static MapEntityPrefab DraggedItemPrefab; + + /// + /// Currently opened hand-held item container like crates + /// + private Item OpenedItem; + + /// + /// When opening an item we save the location of it so we can teleport the dummy character there + /// + private Vector2 oldItemPosition; private GUIFrame wiringToolPanel; private DateTime editorSelectedTime; - private readonly string containerDeleteTag = "containerdelete"; + private const string containerDeleteTag = "containerdelete"; private GUIImage previewImage; @@ -89,24 +119,28 @@ namespace Barotrauma private Mode mode; - public override Camera Cam - { - get { return cam; } - } + private Color backgroundColor = GameSettings.SubEditorBackgroundColor; + + // Prevent the mode from changing + private bool lockMode; - public string GetSubDescription() + private static bool isAutoSaving; + + public override Camera Cam => cam; + + private static string GetSubDescription() { - string localizedDescription = TextManager.Get("submarine.description." + (Submarine.MainSub?.Name ?? ""), true); + string localizedDescription = TextManager.Get("submarine.description." + (Submarine.MainSub?.Info.Name ?? ""), true); if (localizedDescription != null) { return localizedDescription; } - return (Submarine.MainSub == null) ? "" : Submarine.MainSub.Description; + return (Submarine.MainSub == null) ? "" : Submarine.MainSub.Info.Description; } - private string GetTotalHullVolume() + private static string GetTotalHullVolume() { return TextManager.Get("TotalHullVolume") + ":\n" + Hull.hullList.Sum(h => h.Volume); } - private string GetSelectedHullVolume() + private static string GetSelectedHullVolume() { float buoyancyVol = 0.0f; float selectedVol = 0.0f; @@ -135,9 +169,7 @@ namespace Barotrauma return retVal; } - public bool CharacterMode { get { return mode == Mode.Character; } } - - public bool WiringMode { get { return mode == Mode.Wiring; } } + public bool WiringMode => mode == Mode.Wiring; public SubEditorScreen() { @@ -164,7 +196,7 @@ namespace Barotrauma ToolTip = TextManager.Get("back"), OnClicked = (b, d) => { - var msgBox = new GUIMessageBox("", TextManager.Get("PauseMenuQuitVerificationEditor"), new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }) + var msgBox = new GUIMessageBox("", TextManager.Get("PauseMenuQuitVerificationEditor"), new[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }) { UserData = "verificationprompt" }; @@ -189,7 +221,7 @@ namespace Barotrauma new GUIButton(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "OpenButton") { ToolTip = TextManager.Get("OpenSubButton"), - OnClicked = (GUIButton btn, object data) => + OnClicked = (btn, data) => { saveFrame = null; CreateLoadScreen(); @@ -202,8 +234,8 @@ namespace Barotrauma new GUIButton(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "SaveButton") { - ToolTip = TextManager.Get("SaveSubButton"), - OnClicked = (GUIButton btn, object data) => + ToolTip = TextManager.Get("SaveSubButton") + "‖color:125,125,125‖\nCtrl + S‖color:end‖", + OnClicked = (btn, data) => { loadFrame = null; CreateSaveScreen(); @@ -214,6 +246,40 @@ namespace Barotrauma new GUIFrame(new RectTransform(new Vector2(0.01f, 0.9f), paddedTopPanel.RectTransform), style: "VerticalLine"); + new GUIButton(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "TestButton") + { + ToolTip = TextManager.Get("TestSubButton"), + OnClicked = TestSubmarine + }; + + 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") + { + ToolTip = TextManager.Get("SubEditorVisibilityButton") + '\n' + TextManager.Get("SubEditorVisibilityToolTip"), + OnClicked = (btn, userData) => + { + previouslyUsedPanel.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; + } + }; + + var previouslyUsedButton = new GUIButton(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "RecentlyUsedButton") + { + ToolTip = TextManager.Get("PreviouslyUsedLabel"), + OnClicked = (btn, userData) => + { + showEntitiesPanel.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; + } + }; + + new GUIFrame(new RectTransform(new Vector2(0.01f, 0.9f), paddedTopPanel.RectTransform), style: "VerticalLine"); + subNameLabel = new GUITextBlock(new RectTransform(new Vector2(0.3f, 0.9f), paddedTopPanel.RectTransform, Anchor.CenterLeft), TextManager.Get("unspecifiedsubfilename"), font: GUI.LargeFont, textAlignment: Alignment.CenterLeft); @@ -222,7 +288,7 @@ namespace Barotrauma { ToolTip = TextManager.Get("AddSubToolTip") }; - foreach (Submarine sub in Submarine.SavedSubmarines) + foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) { linkedSubBox.AddItem(sub.Name, sub); } @@ -233,68 +299,72 @@ namespace Barotrauma return true; }; - new GUIFrame(new RectTransform(new Vector2(0.01f, 0.9f), paddedTopPanel.RectTransform), style: "VerticalLine"); + var spacing = new GUIFrame(new RectTransform(new Vector2(0.02f, 1.0f), paddedTopPanel.RectTransform), style: null); + new GUIFrame(new RectTransform(new Vector2(0.1f, 0.9f), spacing.RectTransform, Anchor.Center), style: "VerticalLine"); defaultModeTickBox = new GUITickBox(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "EditSubButton") { - ToolTip = TextManager.Get("SubEditorEditingMode"), - OnSelected = (GUITickBox tBox) => + ToolTip = TextManager.Get("SubEditorEditingMode") + "‖color:125,125,125‖\nCtrl + 1‖color:end‖", + OnSelected = tBox => { - if (tBox.Selected) { SetMode(Mode.Default); } - return true; - } - }; + if (!lockMode) + { + if (tBox.Selected) { SetMode(Mode.Default); } - characterModeTickBox = new GUITickBox(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "CharacterModeButton") - { - ToolTip = TextManager.Get("CharacterModeButton") + '\n' + TextManager.Get("CharacterModeToolTip"), - OnSelected = (GUITickBox tBox) => - { - SetMode(tBox.Selected ? Mode.Character : Mode.Default); - return true; + return true; + } + + return false; } }; wiringModeTickBox = new GUITickBox(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "WiringModeButton") { - ToolTip = TextManager.Get("WiringModeButton") + '\n' + TextManager.Get("WiringModeToolTip"), - OnSelected = (GUITickBox tBox) => + ToolTip = TextManager.Get("WiringModeButton") + '\n' + TextManager.Get("WiringModeToolTip") + "‖color:125,125,125‖\nCtrl + 2‖color:end‖", + OnSelected = tBox => { - SetMode(tBox.Selected ? Mode.Wiring : Mode.Default); - return true; + if (!lockMode) + { + SetMode(tBox.Selected ? Mode.Wiring : Mode.Default); + return true; + } + + return false; } }; - new GUIFrame(new RectTransform(new Vector2(0.01f, 0.9f), paddedTopPanel.RectTransform), style: "VerticalLine"); + spacing = new GUIFrame(new RectTransform(new Vector2(0.02f, 1.0f), paddedTopPanel.RectTransform), style: null); + new GUIFrame(new RectTransform(new Vector2(0.1f, 0.9f), spacing.RectTransform, Anchor.Center), style: "VerticalLine"); new GUIButton(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "GenerateWaypointsButton") { ToolTip = TextManager.Get("GenerateWaypointsButton") + '\n' + TextManager.Get("GenerateWaypointsToolTip"), - OnClicked = GenerateWaypoints - }; - - 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") - { - ToolTip = TextManager.Get("SubEditorVisibilityButton") + '\n' + TextManager.Get("SubEditorVisibilityToolTip"), - OnClicked = (btn, userData) => + OnClicked = (btn, userdata) => { - previouslyUsedPanel.Visible = false; - showEntitiesPanel.Visible = !showEntitiesPanel.Visible; - showEntitiesPanel.RectTransform.AbsoluteOffset = new Point(btn.Rect.X, TopPanel.Rect.Height); - return true; - } - }; + if (WayPoint.WayPointList.Any()) + { + var generateWaypointsVerification = new GUIMessageBox("", TextManager.Get("generatewaypointsverification"), new[] { TextManager.Get("ok"), TextManager.Get("cancel") }); + generateWaypointsVerification.Buttons[0].OnClicked = delegate + { + if (GenerateWaypoints()) + { + GUI.AddMessage(TextManager.Get("waypointsgeneratedsuccesfully"), GUI.Style.Green); + } + WayPoint.ShowWayPoints = true; + generateWaypointsVerification.Close(); + return true; + }; + generateWaypointsVerification.Buttons[1].OnClicked = generateWaypointsVerification.Close; + } + else + { + if (GenerateWaypoints()) + { + GUI.AddMessage(TextManager.Get("waypointsgeneratedsuccesfully"), GUI.Style.Green); + } + WayPoint.ShowWayPoints = true; - var previouslyUsedButton = new GUIButton(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "RecentlyUsedButton") - { - ToolTip = TextManager.Get("PreviouslyUsedLabel"), - OnClicked = (btn, userData) => - { - showEntitiesPanel.Visible = false; - previouslyUsedPanel.Visible = !previouslyUsedPanel.Visible; - previouslyUsedPanel.RectTransform.AbsoluteOffset = new Point(btn.Rect.X, TopPanel.Rect.Height); + } return true; } }; @@ -311,7 +381,7 @@ namespace Barotrauma //----------------------------------------------- - previouslyUsedPanel = new GUIFrame(new RectTransform(new Vector2(0.1f, 0.2f), GUI.Canvas, Anchor.TopLeft) { MinSize = new Point(200, 200) }) + previouslyUsedPanel = new GUIFrame(new RectTransform(new Vector2(0.1f, 0.2f), GUI.Canvas) { MinSize = new Point(200, 200) }) { Visible = false }; @@ -325,8 +395,7 @@ namespace Barotrauma showEntitiesPanel = new GUIFrame(new RectTransform(new Vector2(0.08f, 0.5f), GUI.Canvas) { - MinSize = new Point(170, 0), - AbsoluteOffset = new Point(visibilityButton.Rect.X, TopPanel.Rect.Height) + MinSize = new Point(170, 0) }) { Visible = false @@ -408,6 +477,12 @@ namespace Barotrauma Selected = Gap.ShowGaps, OnSelected = (GUITickBox obj) => { Gap.ShowGaps = obj.Selected; return true; }, }; + new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedShowEntitiesPanel.RectTransform), TextManager.Get("mapentitycategory.thalamus")) + { + UserData = "thalamus", + Selected = showThalamus, + OnSelected = (GUITickBox obj) => { showThalamus = obj.Selected; return true; }, + }; showEntitiesTickBoxes.AddRange(paddedShowEntitiesPanel.Children.Select(c => c as GUITickBox)); @@ -492,7 +567,7 @@ namespace Barotrauma }; entityCountPanel.RectTransform.NonScaledSize = new Point( - (int)(paddedEntityCountPanel.RectTransform.Children.Max(c => (int)(c.GUIComponent as GUITextBlock).TextSize.X / 0.75f) / paddedEntityCountPanel.RectTransform.RelativeSize.X), + (int)(paddedEntityCountPanel.RectTransform.Children.Max(c => (int)((GUITextBlock) c.GUIComponent).TextSize.X / 0.75f) / paddedEntityCountPanel.RectTransform.RelativeSize.X), (int)(paddedEntityCountPanel.RectTransform.Children.Sum(c => (int)(c.NonScaledSize.Y * 1.5f) + paddedEntityCountPanel.AbsoluteSpacing) / paddedEntityCountPanel.RectTransform.RelativeSize.Y)); //GUITextBlock.AutoScaleAndNormalize(paddedEntityCountPanel.Children.Where(c => c is GUITextBlock).Cast()); @@ -534,6 +609,7 @@ namespace Barotrauma toggleEntityMenuButton = new GUIButton(new RectTransform(new Vector2(0.15f, 0.08f), EntityMenu.RectTransform, Anchor.TopCenter, Pivot.BottomCenter) { MinSize = new Point(0, 15) }, style: "UIToggleButtonVertical") { + ToolTip = TextManager.Get("EntityMenuToggleTooltip") + "‖color:125,125,125‖\nQ‖color:end‖", OnClicked = (btn, userdata) => { entityMenuOpen = !entityMenuOpen; @@ -577,7 +653,6 @@ namespace Barotrauma { OnClicked = (btn, userdata) => { - if (!string.IsNullOrEmpty(entityFilterBox.Text)) { ClearFilter(); } OpenEntityMenu(null); return true; } @@ -592,7 +667,6 @@ namespace Barotrauma ToolTip = TextManager.Get("MapEntityCategory." + category.ToString()), OnClicked = (btn, userdata) => { - if (!string.IsNullOrEmpty(entityFilterBox.Text)) { ClearFilter(); } MapEntityCategory newCategory = (MapEntityCategory)userdata; OpenEntityMenu(newCategory); return true; @@ -603,7 +677,7 @@ namespace Barotrauma new GUIFrame(new RectTransform(new Vector2(0.8f, 0.01f), paddedTab.RectTransform), style: "HorizontalLine"); - entityList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.9f), paddedTab.RectTransform)) + entityList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.9f), paddedTab.RectTransform), useMouseDownToSelect: true) { OnSelected = SelectPrefab, UseGridLayout = true, @@ -613,6 +687,41 @@ namespace Barotrauma screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); } + private bool TestSubmarine(GUIButton button, object obj) + { + List errorMsgs = new List(); + + if (!Hull.hullList.Any()) + { + errorMsgs.Add(TextManager.Get("NoHullsWarning")); + } + + if (!WayPoint.WayPointList.Any(wp => wp.ShouldBeSaved && wp.SpawnType == SpawnType.Human)) + { + errorMsgs.Add(TextManager.Get("NoHumanSpawnpointWarning")); + } + + if (errorMsgs.Any()) + { + new GUIMessageBox(TextManager.Get("Error"), string.Join("\n\n", errorMsgs), new Vector2(0.25f, 0.0f), new Point(400, 200)); + return true; + } + + backedUpSubInfo = new SubmarineInfo(Submarine.MainSub); + + GameMain.GameScreen.Select(); + + GameSession gameSession = new GameSession(backedUpSubInfo, "", GameModePreset.List.Find(gm => gm.Identifier == "subtest"), null); + gameSession.StartRound(null, false); + + return true; + } + + public void ClearBackedUpSubInfo() + { + backedUpSubInfo = null; + } + private void UpdateEntityList() { entityList.Content.ClearChildren(); @@ -669,7 +778,8 @@ namespace Barotrauma img = new GUIImage(new RectTransform(new Vector2(1.0f, 0.8f), paddedFrame.RectTransform, Anchor.TopCenter), icon) { - CanBeFocused = false, + CanBeFocused = false, + LoadAsynchronously = true, Color = legacy ? iconColor * 0.6f : iconColor }; } @@ -677,7 +787,7 @@ namespace Barotrauma if (ep is ItemAssemblyPrefab itemAssemblyPrefab) { new GUICustomComponent(new RectTransform(new Vector2(1.0f, 0.75f), - paddedFrame.RectTransform, Anchor.TopCenter), onDraw: itemAssemblyPrefab.DrawIcon, onUpdate: null) + paddedFrame.RectTransform, Anchor.TopCenter), onDraw: itemAssemblyPrefab.DrawIcon) { HideElementsOutsideFrame = true, ToolTip = frame.RawToolTip @@ -700,27 +810,30 @@ namespace Barotrauma UserData = ep, OnClicked = (btn, userData) => { - ItemAssemblyPrefab assemblyPrefab = userData as ItemAssemblyPrefab; - var msgBox = new GUIMessageBox( - TextManager.Get("DeleteDialogLabel"), - TextManager.GetWithVariable("DeleteDialogQuestion", "[file]", assemblyPrefab.Name), - new string[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); - msgBox.Buttons[0].OnClicked += (deleteBtn, userData2) => - { - try + ItemAssemblyPrefab assemblyPrefab = (ItemAssemblyPrefab) userData; + if (assemblyPrefab != null) { + var msgBox = new GUIMessageBox( + TextManager.Get("DeleteDialogLabel"), + TextManager.GetWithVariable("DeleteDialogQuestion", "[file]", assemblyPrefab.Name), + new[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); + msgBox.Buttons[0].OnClicked += (deleteBtn, userData2) => { - assemblyPrefab.Delete(); - UpdateEntityList(); - OpenEntityMenu(MapEntityCategory.ItemAssembly); - } - catch (Exception e) - { - DebugConsole.ThrowError(TextManager.GetWithVariable("DeleteFileError", "[file]", assemblyPrefab.Name), e); - } - return true; - }; - msgBox.Buttons[0].OnClicked += msgBox.Close; - msgBox.Buttons[1].OnClicked += msgBox.Close; + try + { + assemblyPrefab.Delete(); + UpdateEntityList(); + OpenEntityMenu(MapEntityCategory.ItemAssembly); + } + catch (Exception e) + { + DebugConsole.ThrowError(TextManager.GetWithVariable("DeleteFileError", "[file]", assemblyPrefab.Name), e); + } + return true; + }; + msgBox.Buttons[0].OnClicked += msgBox.Close; + msgBox.Buttons[1].OnClicked += msgBox.Close; + } + return true; } }; @@ -733,8 +846,8 @@ namespace Barotrauma } } - entityList.Content.RectTransform.SortChildren((i1, i2) => - (i1.GUIComponent.UserData as MapEntityPrefab).Name.CompareTo((i2.GUIComponent.UserData as MapEntityPrefab).Name)); + entityList.Content.RectTransform.SortChildren((i1, i2) => + string.Compare(((MapEntityPrefab) i1.GUIComponent.UserData). Name, (i2.GUIComponent.UserData as MapEntityPrefab)?.Name, StringComparison.Ordinal)); } public override void Select() @@ -744,50 +857,64 @@ namespace Barotrauma GameMain.LightManager.AmbientLight = Level.Loaded?.GenerationParams?.AmbientLightColor ?? LevelGenerationParams.LevelParams?.FirstOrDefault()?.AmbientLightColor ?? - new Color(20, 20, 20, 255); + new Color(20, 20, 20, 255); UpdateEntityList(); - string name = (Submarine.MainSub == null) ? TextManager.Get("unspecifiedsubfilename") : Submarine.MainSub.Name; - subNameLabel.Text = ToolBox.LimitString(name, subNameLabel.Font, subNameLabel.Rect.Width); - - foreach (MapEntityPrefab prefab in MapEntityPrefab.List) + isAutoSaving = false; + if (!wasSelectedBefore) { - prefab.sprite?.EnsureLazyLoaded(); - if (prefab is ItemPrefab itemPrefab) - { - itemPrefab.InventoryIcon?.EnsureLazyLoaded(); - } + OpenEntityMenu(null); + wasSelectedBefore = true; } + if (backedUpSubInfo != null) + { + Submarine.Unload(); + } + + string name = (Submarine.MainSub == null) ? TextManager.Get("unspecifiedsubfilename") : Submarine.MainSub.Info.Name; + if (backedUpSubInfo != null) { name = backedUpSubInfo.Name; } + subNameLabel.Text = ToolBox.LimitString(name, subNameLabel.Font, subNameLabel.Rect.Width); + editorSelectedTime = DateTime.Now; GUI.ForceMouseOn(null); SetMode(Mode.Default); - if (Submarine.MainSub != null) + if (backedUpSubInfo != null) { - Submarine.MainSub.SetPrevTransform(Submarine.MainSub.Position); - Submarine.MainSub.UpdateTransform(); - cam.Position = Submarine.MainSub.Position + Submarine.MainSub.HiddenSubPosition; + Submarine.MainSub = new Submarine(backedUpSubInfo); + backedUpSubInfo = null; } - else + else if (Submarine.MainSub == null) { - Submarine.MainSub = new Submarine(Path.Combine(Submarine.SavePath, TextManager.Get("UnspecifiedSubFileName") + ".sub"), "", false); - cam.Position = Submarine.MainSub.Position; + var subInfo = new SubmarineInfo(); + Submarine.MainSub = new Submarine(subInfo); } - GameMain.SoundManager.SetCategoryGainMultiplier("default", 0.0f, 0); - GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", 0.0f, 0); + Submarine.MainSub.SetPrevTransform(Submarine.MainSub.Position); + Submarine.MainSub.UpdateTransform(interpolate: false); + cam.Position = Submarine.MainSub.Position + Submarine.MainSub.HiddenSubPosition; + + GameMain.SoundManager.SetCategoryGainMultiplier("default", 0.0f); + GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", 0.0f); linkedSubBox.ClearChildren(); - foreach (Submarine sub in Submarine.SavedSubmarines) + foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) { linkedSubBox.AddItem(sub.Name, sub); } cam.UpdateTransform(); + CreateDummyCharacter(); + + if (GameSettings.EnableSubmarineAutoSave) + { + CoroutineManager.StartCoroutine(AutoSaveCoroutine(), "SubEditorAutoSave"); + } + GameAnalyticsManager.SetCustomDimension01("editor"); if (!GameMain.Config.EditorDisclaimerShown) { @@ -795,6 +922,43 @@ namespace Barotrauma } } + /// + /// Coroutine that waits 5 minutes and then runs itself recursively again to save the submarine into a temporary file + /// + /// + /// + private static IEnumerable AutoSaveCoroutine() + { + DateTime target = DateTime.Now.AddMinutes(5); + DateTime tempTarget = DateTime.Now; + + bool wasPaused = false; + + while (DateTime.Now < target && Selected is SubEditorScreen || GameMain.Instance.Paused || wasPaused) + { + if (GameMain.Instance.Paused && !wasPaused) + { + AutoSave(); + tempTarget = DateTime.Now; + wasPaused = true; + } + + if (!GameMain.Instance.Paused && wasPaused) + { + wasPaused = false; + target = target.AddSeconds((DateTime.Now - tempTarget).TotalSeconds); + } + yield return CoroutineStatus.Running; + } + + if (Selected is SubEditorScreen) + { + AutoSave(); + CoroutineManager.StartCoroutine(AutoSaveCoroutine(), "SubEditorAutoSave"); + } + yield return CoroutineStatus.Success; + } + public override void Deselect() { base.Deselect(); @@ -817,8 +981,13 @@ namespace Barotrauma SetMode(Mode.Default); SoundPlayer.OverrideMusicType = null; - GameMain.SoundManager.SetCategoryGainMultiplier("default", GameMain.Config.SoundVolume, 0); - GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", GameMain.Config.SoundVolume, 0); + GameMain.SoundManager.SetCategoryGainMultiplier("default", GameMain.Config.SoundVolume); + GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", GameMain.Config.SoundVolume); + + if (CoroutineManager.IsCoroutineRunning("SubEditorAutoSave")) + { + CoroutineManager.StopCoroutines("SubEditorAutoSave"); + } if (dummyCharacter != null) { @@ -827,13 +996,13 @@ namespace Barotrauma GameMain.World.ProcessChanges(); } - if (GUIMessageBox.MessageBoxes.Any(mbox => (mbox as GUIMessageBox).Tag == containerDeleteTag)) + if (GUIMessageBox.MessageBoxes.Any(mbox => (mbox as GUIMessageBox)?.Tag == containerDeleteTag)) { for (int i = 0; i < GUIMessageBox.MessageBoxes.Count; i++) { GUIMessageBox box = GUIMessageBox.MessageBoxes[i] as GUIMessageBox; - if (box.Tag != containerDeleteTag) continue; - box.Close(); + if (box != null && box.Tag != containerDeleteTag) continue; + box?.Close(); i--; // Take into account the message boxes removing themselves from the list when closed } } @@ -853,9 +1022,9 @@ namespace Barotrauma if (itemNames.Length > 0) { // Multiple prompts open - if (GUIMessageBox.MessageBoxes.Any(mbox => (mbox as GUIMessageBox).Tag == containerDeleteTag)) + if (GUIMessageBox.MessageBoxes.Any(mbox => (mbox as GUIMessageBox)?.Tag == containerDeleteTag)) { - var msgBox = new GUIMessageBox(itemToDelete.Name, TextManager.Get("DeletingContainerWithItems") + itemNames, new string[] { TextManager.Get("Yes"), TextManager.Get("No"), TextManager.Get("YesToAll"), TextManager.Get("NoToAll") }, tag: containerDeleteTag); + var msgBox = new GUIMessageBox(itemToDelete.Name, TextManager.Get("DeletingContainerWithItems") + itemNames, new[] { TextManager.Get("Yes"), TextManager.Get("No"), TextManager.Get("YesToAll"), TextManager.Get("NoToAll") }, tag: containerDeleteTag); // Yes msgBox.Buttons[0].OnClicked = (btn, userdata) => @@ -890,9 +1059,9 @@ namespace Barotrauma for (int i = 0; i < GUIMessageBox.MessageBoxes.Count; i++) { GUIMessageBox box = GUIMessageBox.MessageBoxes[i] as GUIMessageBox; - if (box.Tag != msgBox.Tag || box == msgBox) continue; - GUIButton button = box.Buttons[0]; - button.OnClicked(button, button.UserData); + if (box?.Tag != msgBox.Tag || box == msgBox) continue; + GUIButton button = box?.Buttons[0]; + button?.OnClicked(button, button.UserData); i--; // Take into account the message boxes removing themselves from the list when closed } @@ -907,9 +1076,9 @@ namespace Barotrauma for (int i = 0; i < GUIMessageBox.MessageBoxes.Count; i++) { GUIMessageBox box = GUIMessageBox.MessageBoxes[i] as GUIMessageBox; - if (box.Tag != msgBox.Tag || box == msgBox) continue; - GUIButton button = box.Buttons[1]; - button.OnClicked(button, button.UserData); + if (box?.Tag != msgBox.Tag || box == msgBox) continue; + GUIButton button = box?.Buttons[1]; + button?.OnClicked(button, button.UserData); i--; // Take into account the message boxes removing themselves from the list when closed } @@ -931,7 +1100,7 @@ namespace Barotrauma } else // Single prompt { - var msgBox = new GUIMessageBox(itemToDelete.Name, TextManager.Get("DeletingContainerWithItems") + itemNames, new string[] { TextManager.Get("Yes"), TextManager.Get("No") }, tag: containerDeleteTag); + var msgBox = new GUIMessageBox(itemToDelete.Name, TextManager.Get("DeletingContainerWithItems") + itemNames, new[] { TextManager.Get("Yes"), TextManager.Get("No") }, tag: containerDeleteTag); // Yes msgBox.Buttons[0].OnClicked = (btn, userdata) => @@ -984,6 +1153,46 @@ namespace Barotrauma GameMain.World.ProcessChanges(); } + /// + /// Saves the current main sub into a temporary file outside of the Submarines/ folder + /// + /// + /// The saving is ran in another thread to avoid lag spikes + private static void AutoSave() + { + if (MapEntity.mapEntityList.Any() && GameSettings.EnableSubmarineAutoSave && !isAutoSaving) + { + if (Submarine.MainSub != null) + { + isAutoSaving = true; + string filePath = Path.Combine(SubmarineInfo.SavePath, ".AutoSaves"); + if (!Directory.Exists(filePath)) + { + var e = Directory.CreateDirectory(filePath); + e.Attributes = FileAttributes.Directory | FileAttributes.Hidden; + if (!e.Exists) { return; } + } + + XDocument doc = new XDocument(new XElement("Submarine")); + Submarine.MainSub.SaveToXElement(doc.Root); + Thread saveThread = new Thread(start => + { + try + { + SaveUtil.CompressStringToFile(Path.Combine(filePath, "AutoSave.sub"), doc.ToString()); + CrossThread.RequestExecutionOnMainThread(() => GUI.AddMessage(TextManager.Get("AutoSaved"), GUI.Style.Green, playSound: false)); + } + catch (Exception e) + { + CrossThread.RequestExecutionOnMainThread(() => DebugConsole.ThrowError("Saving submarine \"" + filePath + "\" failed!", e)); + } + isAutoSaving = false; + }) { Name = "Auto Save Thread" }; + saveThread.Start(); + } + } + } + private bool SaveSub(GUIButton button, object obj) { if (string.IsNullOrWhiteSpace(nameBox.Text)) @@ -992,27 +1201,39 @@ namespace Barotrauma nameBox.Flash(); return false; } - - foreach (char illegalChar in Path.GetInvalidFileNameChars()) + var result = SaveSubToFile(nameBox.Text); + saveFrame = null; + return result; + } + + private bool SaveSubToFile(string name) + { + if (string.IsNullOrWhiteSpace(name)) { - if (nameBox.Text.Contains(illegalChar)) - { - GUI.AddMessage(TextManager.GetWithVariable("SubNameIllegalCharsWarning", "[illegalchar]", illegalChar.ToString()), GUI.Style.Red); - nameBox.Flash(); - return false; - } + GUI.AddMessage(TextManager.Get("SubNameMissingWarning"), GUI.Style.Red); + return false; } - string savePath = nameBox.Text + ".sub"; - string prevSavePath = null; - if (Submarine.MainSub != null) + foreach (var illegalChar in Path.GetInvalidFileNameChars()) { - prevSavePath = Submarine.MainSub.FilePath; - savePath = Path.Combine(Path.GetDirectoryName(Submarine.MainSub.FilePath), savePath); + if (!name.Contains(illegalChar)) continue; + GUI.AddMessage(TextManager.GetWithVariable("SubNameIllegalCharsWarning", "[illegalchar]", illegalChar.ToString()), GUI.Style.Red); + return false; + } + + Submarine.MainSub.Info.Name = name; + + string savePath = name + ".sub"; + string prevSavePath = null; + if (!string.IsNullOrEmpty(Submarine.MainSub?.Info.FilePath) && + Submarine.MainSub.Info.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) + { + prevSavePath = Submarine.MainSub.Info.FilePath.CleanUpPath(); + savePath = Path.Combine(Path.GetDirectoryName(Submarine.MainSub.Info.FilePath), savePath).CleanUpPath(); } else { - savePath = Path.Combine(Submarine.SavePath, savePath); + savePath = Path.Combine(SubmarineInfo.SavePath, savePath); } #if !DEBUG @@ -1020,8 +1241,8 @@ namespace Barotrauma if (vanilla != null) { var vanillaSubs = vanilla.GetFilesOfType(ContentType.Submarine); - string pathToCompare = savePath.Replace(@"\", @"/").ToLowerInvariant(); - if (vanillaSubs.Any(sub => sub.Replace(@"\", @"/").ToLowerInvariant() == pathToCompare)) + string pathToCompare = savePath.Replace(@"\", @"/"); + if (vanillaSubs.Any(sub => sub.Replace(@"\", @"/").Equals(pathToCompare, StringComparison.OrdinalIgnoreCase))) { GUI.AddMessage(TextManager.Get("CannotEditVanillaSubs"), GUI.Style.Red, font: GUI.LargeFont); return false; @@ -1029,44 +1250,53 @@ namespace Barotrauma } #endif - if (previewImage.Sprite?.Texture != null) + if (Submarine.MainSub != null) { - using (MemoryStream imgStream = new MemoryStream()) + if (previewImage?.Sprite?.Texture != null) { - previewImage.Sprite.Texture.SaveAsPng(imgStream, previewImage.Sprite.Texture.Width, previewImage.Sprite.Texture.Height); - Submarine.SaveCurrent(savePath, imgStream); + bool savePreviewImage = true; + using MemoryStream imgStream = new MemoryStream(); + try + { + previewImage.Sprite.Texture.SaveAsPng(imgStream, previewImage.Sprite.Texture.Width, previewImage.Sprite.Texture.Height); + } + catch (Exception e) + { + DebugConsole.ThrowError($"Saving the preview image of the submarine \"{Submarine.MainSub.Info.Name}\" failed.", e); + savePreviewImage = false; + } + Submarine.MainSub.SaveAs(savePath, savePreviewImage ? imgStream : null); } - } - else - { - Submarine.SaveCurrent(savePath); - } - Submarine.MainSub.CheckForErrors(); - - GUI.AddMessage(TextManager.GetWithVariable("SubSavedNotification", "[filepath]", Submarine.MainSub.FilePath), GUI.Style.Green); + else + { + Submarine.MainSub.SaveAs(savePath); + } + + Submarine.MainSub.CheckForErrors(); + + GUI.AddMessage(TextManager.GetWithVariable("SubSavedNotification", "[filepath]", savePath), GUI.Style.Green); - Submarine.RefreshSavedSub(savePath); - if (prevSavePath != null && prevSavePath != savePath) - { - Submarine.RefreshSavedSub(prevSavePath); + SubmarineInfo.RefreshSavedSub(savePath); + if (prevSavePath != null && prevSavePath != savePath) { SubmarineInfo.RefreshSavedSub(prevSavePath); } + + linkedSubBox.ClearChildren(); + foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) { linkedSubBox.AddItem(sub.Name, sub); } + + subNameLabel.Text = ToolBox.LimitString(Submarine.MainSub.Info.Name, subNameLabel.Font, subNameLabel.Rect.Width); } - linkedSubBox.ClearChildren(); - foreach (Submarine sub in Submarine.SavedSubmarines) - { - linkedSubBox.AddItem(sub.Name, sub); - } - - subNameLabel.Text = ToolBox.LimitString(Submarine.MainSub.Name, subNameLabel.Font, subNameLabel.Rect.Width); - - saveFrame = null; - return false; } - private void CreateSaveScreen() + private void CreateSaveScreen(bool quickSave = false) { - SetMode(Mode.Default); + if (saveFrame != null) { return; } + + if (!quickSave) + { + CloseItem(); + SetMode(Mode.Default); + } saveFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas), style: "GUIBackgroundBlocker") { @@ -1165,15 +1395,15 @@ namespace Barotrauma crewSizeMin.OnValueChanged += (numberInput) => { crewSizeMax.IntValue = Math.Max(crewSizeMax.IntValue, numberInput.IntValue); - Submarine.MainSub.RecommendedCrewSizeMin = crewSizeMin.IntValue; - Submarine.MainSub.RecommendedCrewSizeMax = crewSizeMax.IntValue; + Submarine.MainSub.Info.RecommendedCrewSizeMin = crewSizeMin.IntValue; + Submarine.MainSub.Info.RecommendedCrewSizeMax = crewSizeMax.IntValue; }; crewSizeMax.OnValueChanged += (numberInput) => { crewSizeMin.IntValue = Math.Min(crewSizeMin.IntValue, numberInput.IntValue); - Submarine.MainSub.RecommendedCrewSizeMin = crewSizeMin.IntValue; - Submarine.MainSub.RecommendedCrewSizeMax = crewSizeMax.IntValue; + Submarine.MainSub.Info.RecommendedCrewSizeMin = crewSizeMin.IntValue; + Submarine.MainSub.Info.RecommendedCrewSizeMax = crewSizeMax.IntValue; }; var crewExpArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.04f), leftColumn.RectTransform), isHorizontal: true) @@ -1196,7 +1426,7 @@ namespace Barotrauma if (currentIndex < 0) currentIndex = crewExperienceLevels.Length - 1; experienceText.UserData = crewExperienceLevels[currentIndex]; experienceText.Text = TextManager.Get(crewExperienceLevels[currentIndex]); - Submarine.MainSub.RecommendedCrewExperience = (string)experienceText.UserData; + Submarine.MainSub.Info.RecommendedCrewExperience = (string)experienceText.UserData; return true; }; @@ -1207,18 +1437,18 @@ namespace Barotrauma if (currentIndex >= crewExperienceLevels.Length) currentIndex = 0; experienceText.UserData = crewExperienceLevels[currentIndex]; experienceText.Text = TextManager.Get(crewExperienceLevels[currentIndex]); - Submarine.MainSub.RecommendedCrewExperience = (string)experienceText.UserData; + Submarine.MainSub.Info.RecommendedCrewExperience = (string)experienceText.UserData; return true; }; if (Submarine.MainSub != null) { - int min = Submarine.MainSub.RecommendedCrewSizeMin; - int max = Submarine.MainSub.RecommendedCrewSizeMax; + int min = Submarine.MainSub.Info.RecommendedCrewSizeMin; + int max = Submarine.MainSub.Info.RecommendedCrewSizeMax; crewSizeMin.IntValue = min; crewSizeMax.IntValue = max; - experienceText.UserData = string.IsNullOrEmpty(Submarine.MainSub.RecommendedCrewExperience) ? - crewExperienceLevels[0] : Submarine.MainSub.RecommendedCrewExperience; + experienceText.UserData = string.IsNullOrEmpty(Submarine.MainSub.Info.RecommendedCrewExperience) ? + crewExperienceLevels[0] : Submarine.MainSub.Info.RecommendedCrewExperience; experienceText.Text = TextManager.Get((string)experienceText.UserData); } @@ -1227,7 +1457,7 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), rightColumn.RectTransform), TextManager.Get("SubPreviewImage"), font: GUI.SubHeadingFont); var previewImageHolder = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.5f), rightColumn.RectTransform), style: null) { Color = Color.Black, CanBeFocused = false }; - previewImage = new GUIImage(new RectTransform(Vector2.One, previewImageHolder.RectTransform), Submarine.MainSub?.PreviewImage, scaleToFit: true); + previewImage = new GUIImage(new RectTransform(Vector2.One, previewImageHolder.RectTransform), Submarine.MainSub?.Info.PreviewImage, scaleToFit: true); var previewImageButtonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), rightColumn.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.05f }; @@ -1241,7 +1471,7 @@ namespace Barotrauma previewImage.Sprite = new Sprite(TextureLoader.FromStream(imgStream), null, null); if (Submarine.MainSub != null) { - Submarine.MainSub.PreviewImage = previewImage.Sprite; + Submarine.MainSub.Info.PreviewImage = previewImage.Sprite; } } return true; @@ -1263,7 +1493,7 @@ namespace Barotrauma previewImage.Sprite = new Sprite(file, sourceRectangle: null); if (Submarine.MainSub != null) { - Submarine.MainSub.PreviewImage = previewImage.Sprite; + Submarine.MainSub.Info.PreviewImage = previewImage.Sprite; } }; FileSelection.ClearFileTypeFilters(); @@ -1293,7 +1523,7 @@ namespace Barotrauma var tagTickBox = new GUITickBox(new RectTransform(new Vector2(0.2f, 0.2f), tagContainer.Content.RectTransform), tagStr, font: GUI.SmallFont) { - Selected = Submarine.MainSub == null ? false : Submarine.MainSub.HasTag(tag), + Selected = Submarine.MainSub != null && Submarine.MainSub.Info.HasTag(tag), UserData = tag, OnSelected = (GUITickBox tickBox) => @@ -1301,11 +1531,11 @@ namespace Barotrauma if (Submarine.MainSub == null) return false; if (tickBox.Selected) { - Submarine.MainSub.AddTag((SubmarineTag)tickBox.UserData); + Submarine.MainSub.Info.AddTag((SubmarineTag)tickBox.UserData); } else { - Submarine.MainSub.RemoveTag((SubmarineTag)tickBox.UserData); + Submarine.MainSub.Info.RemoveTag((SubmarineTag)tickBox.UserData); } return true; } @@ -1318,34 +1548,36 @@ namespace Barotrauma var contentPackList = new GUIListBox(new RectTransform(new Vector2(0.5f, 1.0f - contentPackagesLabel.RectTransform.RelativeSize.Y), horizontalArea.RectTransform, Anchor.BottomRight)); - List contentPacks = Submarine.MainSub.RequiredContentPackages.ToList(); - foreach (ContentPackage contentPack in ContentPackage.List) - { - //don't show content packages that only define submarine files - //(it doesn't make sense to require another sub to be installed to install this one) - if (contentPack.Files.All(cp => cp.Type == ContentType.Submarine)) { continue; } - if (!contentPacks.Contains(contentPack.Name)) { contentPacks.Add(contentPack.Name); } - } + if (Submarine.MainSub != null) { + List contentPacks = Submarine.MainSub.Info.RequiredContentPackages.ToList(); + foreach (ContentPackage contentPack in ContentPackage.List) + { + //don't show content packages that only define submarine files + //(it doesn't make sense to require another sub to be installed to install this one) + if (contentPack.Files.All(cp => cp.Type == ContentType.Submarine)) { continue; } + if (!contentPacks.Contains(contentPack.Name)) { contentPacks.Add(contentPack.Name); } + } - foreach (string contentPackageName in contentPacks) - { - var cpTickBox = new GUITickBox(new RectTransform(new Vector2(0.2f, 0.2f), contentPackList.Content.RectTransform), contentPackageName, font: GUI.SmallFont) + foreach (string contentPackageName in contentPacks) { - Selected = Submarine.MainSub.RequiredContentPackages.Contains(contentPackageName), - UserData = contentPackageName - }; - cpTickBox.OnSelected += (GUITickBox tickBox) => - { - if (tickBox.Selected) + var cpTickBox = new GUITickBox(new RectTransform(new Vector2(0.2f, 0.2f), contentPackList.Content.RectTransform), contentPackageName, font: GUI.SmallFont) { - Submarine.MainSub.RequiredContentPackages.Add((string)tickBox.UserData); - } - else + Selected = Submarine.MainSub.Info.RequiredContentPackages.Contains(contentPackageName), + UserData = contentPackageName + }; + cpTickBox.OnSelected += tickBox => { - Submarine.MainSub.RequiredContentPackages.Remove((string)tickBox.UserData); - } - return true; - }; + if (tickBox.Selected) + { + Submarine.MainSub.Info.RequiredContentPackages.Add((string)tickBox.UserData); + } + else + { + Submarine.MainSub.Info.RequiredContentPackages.Remove((string)tickBox.UserData); + } + return true; + }; + } } @@ -1368,8 +1600,10 @@ namespace Barotrauma }; paddedSaveFrame.Recalculate(); leftColumn.Recalculate(); - descriptionBox.Text = Submarine.MainSub == null ? "" : Submarine.MainSub.Description; + descriptionBox.Text = Submarine.MainSub == null ? "" : Submarine.MainSub.Info.Description; submarineDescriptionCharacterCount.Text = descriptionBox.Text.Length + " / " + submarineDescriptionLimit; + + if (quickSave) { SaveSub(saveButton, saveButton.UserData); } } @@ -1428,6 +1662,27 @@ namespace Barotrauma }; } + /// + /// Loads an item assembly and only returns items which are not inside other inventories. + /// This is to prevent us from trying to place for example Oxygen Tanks inside an inventory + /// when it's already inside a diving suit. + /// + /// + /// + private List LoadItemAssemblyInventorySafe(ItemAssemblyPrefab assemblyPrefab) + { + var realItems = assemblyPrefab.CreateInstance(Vector2.Zero, Submarine.MainSub); + var itemInstance = new List(); + realItems.ForEach(entity => + { + if (entity is Item it && it.ParentInventory == null) + { + itemInstance.Add(it); + } + }); + return itemInstance; + } + private bool SaveAssembly(GUIButton button, object obj) { if (string.IsNullOrWhiteSpace(nameBox.Text)) @@ -1456,7 +1711,7 @@ namespace Barotrauma if (File.Exists(filePath)) { - var msgBox = new GUIMessageBox(TextManager.Get("Warning"), TextManager.Get("ItemAssemblyFileExistsWarning"), new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); + var msgBox = new GUIMessageBox(TextManager.Get("Warning"), TextManager.Get("ItemAssemblyFileExistsWarning"), new[] { TextManager.Get("Yes"), TextManager.Get("No") }); msgBox.Buttons[0].OnClicked = (btn, userdata) => { msgBox.Close(); @@ -1484,8 +1739,9 @@ namespace Barotrauma return false; } - private bool CreateLoadScreen() + private void CreateLoadScreen() { + CloseItem(); SetMode(Mode.Default); loadFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas), style: "GUIBackgroundBlocker") @@ -1493,11 +1749,15 @@ namespace Barotrauma OnClicked = (btn, userdata) => { if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) loadFrame = null; return true; }, }; - var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.2f, 0.36f), loadFrame.RectTransform, Anchor.Center) { MinSize = new Point(350, 500) }); + var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.5f), loadFrame.RectTransform, Anchor.Center) { MinSize = new Point(350, 500) }); var paddedLoadFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), innerFrame.RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.02f }; - var deleteButtonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), paddedLoadFrame.RectTransform, Anchor.Center)); + var deleteButtonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.2f), paddedLoadFrame.RectTransform, Anchor.Center)) + { + RelativeSpacing = 0.1f, + Stretch = true + }; var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedLoadFrame.RectTransform), font: GUI.Font, createClearButton: true); var searchTitle = new GUITextBlock(new RectTransform(Vector2.One, searchBox.RectTransform), TextManager.Get("serverlog.filter"), @@ -1508,7 +1768,7 @@ namespace Barotrauma }; searchTitle.TextColor *= 0.5f; - var subList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), paddedLoadFrame.RectTransform)) + var subList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.7f), paddedLoadFrame.RectTransform)) { ScrollBarVisible = true, OnSelected = (GUIComponent selected, object userData) => @@ -1518,7 +1778,7 @@ namespace Barotrauma #if DEBUG deleteBtn.Enabled = true; #else - deleteBtn.Enabled = userData is Submarine sub && !sub.IsVanillaSubmarine(); + deleteBtn.Enabled = userData is SubmarineInfo subInfo && !subInfo.IsVanillaSubmarine(); #endif } return true; @@ -1529,7 +1789,7 @@ namespace Barotrauma searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; searchBox.OnTextChanged += (textBox, text) => { FilterSubs(subList, text); return true; }; - foreach (Submarine sub in Submarine.SavedSubmarines) + foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) { GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), subList.Content.RectTransform) { MinSize = new Point(0, 30) }, ToolBox.LimitString(sub.Name, GUI.Font, subList.Rect.Width - 80)) @@ -1549,7 +1809,7 @@ namespace Barotrauma } } - var deleteButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), deleteButtonHolder.RectTransform, Anchor.TopCenter), + var deleteButton = new GUIButton(new RectTransform(Vector2.One, deleteButtonHolder.RectTransform, Anchor.TopCenter), TextManager.Get("Delete")) { Enabled = false, @@ -1559,11 +1819,23 @@ namespace Barotrauma { if (subList.SelectedComponent != null) { - TryDeleteSub(subList.SelectedComponent.UserData as Submarine); + TryDeleteSub(subList.SelectedComponent.UserData as SubmarineInfo); } deleteButton.Enabled = false; return true; }; + + var loadAutoSave = new GUIButton(new RectTransform(Vector2.One, deleteButtonHolder.RectTransform, Anchor.BottomCenter), TextManager.Get("LoadAutoSave")) + { + Enabled = File.Exists(Path.Combine(SubmarineInfo.SavePath, ".AutoSaves", "AutoSave.sub")), + ToolTip = TextManager.Get("LoadAutoSaveTooltip"), + UserData = "loadautosave", + OnClicked = (button, o) => + { + LoadAutoSave(); + return true; + } + }; var controlBtnHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), paddedLoadFrame.RectTransform), isHorizontal: true) { RelativeSpacing = 0.2f, Stretch = true }; @@ -1582,16 +1854,58 @@ namespace Barotrauma { OnClicked = LoadSub }; - - return true; } private void FilterSubs(GUIListBox subList, string filter) { foreach (GUIComponent child in subList.Content.Children) { - if (!(child.UserData is Submarine sub)) { return; } - child.Visible = string.IsNullOrEmpty(filter) ? true : sub.Name.ToLower().Contains(filter.ToLower()); + if (!(child.UserData is SubmarineInfo sub)) { return; } + child.Visible = string.IsNullOrEmpty(filter) || sub.Name.ToLower().Contains(filter.ToLower()); + } + } + + /// + /// Recovers the auto saved submarine + /// + /// + private void LoadAutoSave() + { + string filePath = Path.Combine(SubmarineInfo.SavePath, ".AutoSaves", "AutoSave.sub"); + + var loadedSub = Submarine.Load(new SubmarineInfo(filePath), true); + + // set the submarine file path to the "default" value + loadedSub.Info.FilePath = Path.Combine(SubmarineInfo.SavePath, $"{TextManager.Get("UnspecifiedSubFileName")}.sub"); + loadedSub.Info.Name = TextManager.Get("UnspecifiedSubFileName"); + try + { + loadedSub.Info.Name = loadedSub.Info.SubmarineElement.GetAttributeString("name", loadedSub.Info.Name); + } + catch (Exception e) + { + DebugConsole.ThrowError("Failed to find a name for the submarine.", e); + } + Submarine.MainSub = loadedSub; + Submarine.MainSub.SetPrevTransform(Submarine.MainSub.Position); + Submarine.MainSub.UpdateTransform(); + Submarine.MainSub.Info.Name = loadedSub.Info.Name; + subNameLabel.Text = ToolBox.LimitString(loadedSub.Info.Name, subNameLabel.Font, subNameLabel.Rect.Width); + + CreateDummyCharacter(); + + cam.Position = Submarine.MainSub.Position + Submarine.MainSub.HiddenSubPosition; + + loadFrame = null; + + //turn off lights that are inside an inventory (cabinet for example) + foreach (Item item in Item.ItemList) + { + var lightComponent = item.GetComponent(); + if (lightComponent != null) + { + lightComponent.Light.Enabled = item.ParentInventory == null; + } } } @@ -1611,14 +1925,16 @@ namespace Barotrauma } if (subList.SelectedComponent == null) { return false; } - if (!(subList.SelectedComponent.UserData is Submarine selectedSub)) { return false; } + if (!(subList.SelectedComponent.UserData is SubmarineInfo selectedSubInfo)) { return false; } - selectedSub.Load(true); + Submarine.Unload(); + var selectedSub = new Submarine(selectedSubInfo); Submarine.MainSub = selectedSub; - Submarine.MainSub.SetPrevTransform(Submarine.MainSub.Position); - Submarine.MainSub.UpdateTransform(); + Submarine.MainSub.UpdateTransform(interpolate: false); + + CreateDummyCharacter(); - string name = Submarine.MainSub.Name; + string name = Submarine.MainSub.Info.Name; subNameLabel.Text = ToolBox.LimitString(name, subNameLabel.Font, subNameLabel.Rect.Width); cam.Position = Submarine.MainSub.Position + Submarine.MainSub.HiddenSubPosition; @@ -1635,10 +1951,10 @@ namespace Barotrauma } } - if (selectedSub.GameVersion < new Version("0.8.9.0")) + if (selectedSub.Info.GameVersion < new Version("0.8.9.0")) { var adjustLightsPrompt = new GUIMessageBox(TextManager.Get("Warning"), TextManager.Get("AdjustLightsPrompt"), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); + new[] { TextManager.Get("Yes"), TextManager.Get("No") }); adjustLightsPrompt.Buttons[0].OnClicked += adjustLightsPrompt.Close; adjustLightsPrompt.Buttons[0].OnClicked += (btn, userdata) => { @@ -1657,7 +1973,7 @@ namespace Barotrauma return true; } - private void TryDeleteSub(Submarine sub) + private void TryDeleteSub(SubmarineInfo sub) { if (sub == null) { return; } @@ -1682,9 +1998,9 @@ namespace Barotrauma { try { - sub.Remove(); + sub.Dispose(); File.Delete(sub.FilePath); - Submarine.RefreshSavedSubs(); + SubmarineInfo.RefreshSavedSubs(); CreateLoadScreen(); } catch (Exception e) @@ -1697,19 +2013,19 @@ namespace Barotrauma msgBox.Buttons[1].OnClicked += msgBox.Close; } - private bool OpenEntityMenu(MapEntityCategory? selectedCategory) + private void OpenEntityMenu(MapEntityCategory? entityCategory) { foreach (GUIButton categoryButton in entityCategoryButtons) { - categoryButton.Selected = selectedCategory.HasValue ? - categoryButton.UserData is MapEntityCategory category && selectedCategory.Value == category : + categoryButton.Selected = entityCategory.HasValue ? + categoryButton.UserData is MapEntityCategory category && entityCategory.Value == category : categoryButton.UserData == null; - string categoryName = selectedCategory.HasValue ? selectedCategory.Value.ToString() : "All"; + string categoryName = entityCategory.HasValue ? entityCategory.Value.ToString() : "All"; selectedCategoryText.Text = TextManager.Get("MapEntityCategory." + categoryName); selectedCategoryButton.ApplyStyle(GUI.Style.GetComponentStyle("CategoryButton." + categoryName)); } - this.selectedCategory = selectedCategory; + selectedCategory = entityCategory; SetMode(Mode.Default); @@ -1723,74 +2039,71 @@ namespace Barotrauma foreach (GUIComponent child in entityList.Content.Children) { - child.Visible = !selectedCategory.HasValue || ((MapEntityPrefab)child.UserData).Category == selectedCategory; + child.Visible = !entityCategory.HasValue || ((MapEntityPrefab) child.UserData).Category == entityCategory; + if (child.Visible && dummyCharacter?.SelectedConstruction?.OwnInventory != null) + { + child.Visible = child.UserData is MapEntityPrefab item && IsItemPrefab(item); + } } + + if (!string.IsNullOrEmpty(entityFilterBox.Text)) { FilterEntities(entityFilterBox.Text); } + entityList.UpdateScrollBarSize(); entityList.BarScroll = 0.0f; - - return true; } - private bool FilterEntities(string filter) + private void FilterEntities(string filter) { if (string.IsNullOrWhiteSpace(filter)) { - entityList.Content.Children.ForEach(c => c.Visible = !selectedCategory.HasValue || selectedCategory == ((MapEntityPrefab)c.UserData).Category); - return true; + entityList.Content.Children.ForEach(c => + { + c.Visible = !selectedCategory.HasValue || selectedCategory == ((MapEntityPrefab) c.UserData).Category; + if (c.Visible && dummyCharacter?.SelectedConstruction?.OwnInventory != null) + { + c.Visible = c.UserData is MapEntityPrefab item && IsItemPrefab(item); + } + }); + entityList.UpdateScrollBarSize(); + entityList.BarScroll = 0.0f; + + return; } filter = filter.ToLower(); foreach (GUIComponent child in entityList.Content.Children) { var textBlock = child.GetChild(); - child.Visible = - (!selectedCategory.HasValue || selectedCategory == ((MapEntityPrefab)child.UserData).Category) && - ((MapEntityPrefab)child.UserData).Name.ToLower().Contains(filter); + child.Visible = + (!selectedCategory.HasValue || selectedCategory == ((MapEntityPrefab) child.UserData).Category) && + ((MapEntityPrefab) child.UserData).Name.ToLower().Contains(filter); + + if (child.Visible && dummyCharacter?.SelectedConstruction?.OwnInventory != null) + { + child.Visible = child.UserData is MapEntityPrefab item && IsItemPrefab(item); + } } entityList.UpdateScrollBarSize(); entityList.BarScroll = 0.0f; - - return true; } - public bool ClearFilter() + private void ClearFilter() { FilterEntities(""); entityList.UpdateScrollBarSize(); entityList.BarScroll = 0.0f; entityFilterBox.Text = ""; - return true; } - public void SetMode(Mode mode) + public void SetMode(Mode newMode) { - if (mode == this.mode) { return; } - this.mode = mode; + if (newMode == mode) { return; } + mode = newMode; - defaultModeTickBox.Selected = mode == Mode.Default; - defaultModeTickBox.CanBeFocused = !defaultModeTickBox.Selected; - - characterModeTickBox.Selected = mode == Mode.Character; - wiringModeTickBox.Selected = mode == Mode.Wiring; - - switch (mode) - { - case Mode.Character: - CreateDummyCharacter(); - break; - case Mode.Wiring: - CreateDummyCharacter(); - var item = new Item(MapEntityPrefab.Find(null, "screwdriver") as ItemPrefab, Vector2.Zero, null); - dummyCharacter.Inventory.TryPutItem(item, null, new List() { InvSlotType.RightHand }); - wiringToolPanel = CreateWiringPanel(); - break; - default: - if (dummyCharacter != null) - { - RemoveDummyCharacter(); - } - break; - } + lockMode = true; + defaultModeTickBox.Selected = newMode == Mode.Default; + wiringModeTickBox.Selected = newMode == Mode.Wiring; + lockMode = false; foreach (MapEntity me in MapEntity.mapEntityList) { @@ -1799,17 +2112,23 @@ namespace Barotrauma MapEntity.DeselectAll(); MapEntity.FilteredSelectedList.Clear(); + + CreateDummyCharacter(); + if (newMode == Mode.Wiring) + { + var item = new Item(MapEntityPrefab.Find(null, "screwdriver") as ItemPrefab, Vector2.Zero, null); + dummyCharacter.Inventory.TryPutItem(item, null, new List() { InvSlotType.RightHand }); + wiringToolPanel = CreateWiringPanel(); + } } private void RemoveDummyCharacter() { - if (dummyCharacter == null) { return; } + if (dummyCharacter == null || dummyCharacter.Removed) { return; } foreach (Item item in dummyCharacter.Inventory.Items) { - if (item == null) { continue; } - - item.Remove(); + item?.Remove(); } dummyCharacter.Remove(); @@ -1821,53 +2140,102 @@ namespace Barotrauma List targets = MapEntity.mapEntityList.Any(me => me.IsHighlighted && !MapEntity.SelectedList.Contains(me)) ? MapEntity.mapEntityList.Where(me => me.IsHighlighted).ToList() : new List(MapEntity.SelectedList); - + contextMenu = new GUIListBox(new RectTransform(new Vector2(0.1f, 0.1f), GUI.Canvas) { - MinSize = new Point(180,0), ScreenSpaceOffset = PlayerInput.MousePosition.ToPoint() - }, style: "GUIToolTip"); - - new GUITextBlock(new RectTransform(new Point(contextMenu.Rect.Width, (int)(18 * GUI.Scale)), contextMenu.Content.RectTransform), - TextManager.Get("editor.cut"), font: GUI.SmallFont) + }, style: "GUIToolTip") { - UserData = "cut", - Enabled = targets.Count > 0 - }; - new GUITextBlock(new RectTransform(new Point(contextMenu.Rect.Width, (int)(18 * GUI.Scale)), contextMenu.Content.RectTransform), - TextManager.Get("editor.copytoclipboard"), font: GUI.SmallFont) - { - UserData = "copy", - Enabled = targets.Count > 0 - }; - new GUITextBlock(new RectTransform(new Point(contextMenu.Rect.Width, (int)(18 * GUI.Scale)), contextMenu.Content.RectTransform), - TextManager.Get("editor.paste"), font: GUI.SmallFont) - { - UserData = "paste", - Enabled = MapEntity.CopiedList.Any() - }; - new GUITextBlock(new RectTransform(new Point(contextMenu.Rect.Width, (int)(18 * GUI.Scale)), contextMenu.Content.RectTransform), - TextManager.Get("delete"), font: GUI.SmallFont) - { - UserData = "delete", - Enabled = targets.Count > 0 + Padding = new Vector4(5) }; - foreach (GUITextBlock child in contextMenu.Content.Children) + Item target = null; + + var single = targets.Count == 1 ? targets.Single() : null; + if (single is Item item && item.Components.Any(ic => !(ic is ConnectionPanel) && !(ic is Repairable) && ic.GuiFrame != null)) { - if (!child.Enabled) { child.TextColor *= 0.5f; } + // Do not offer the ability to open the inventory if the inventory should never be drawn + var container = item.GetComponent(); + if (container == null || container.DrawInventory) { target = item; } + } + + // Holding shift brings up special context menu options + if (PlayerInput.IsShiftDown()) + { + new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), + TextManager.Get("CharacterEditor.EditBackgroundColor"), font: GUI.SmallFont) + { + UserData = "bgcolor" + }; + } + else + { + new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), + TextManager.Get("label.openlabel"), font: GUI.SmallFont) + { + UserData = "open", + Enabled = target != null + }; + + new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), + TextManager.Get("editor.cut"), font: GUI.SmallFont) + { + UserData = "cut", + Enabled = targets.Count > 0 + }; + + new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), + TextManager.Get("editor.copytoclipboard"), font: GUI.SmallFont) + { + UserData = "copy", + Enabled = targets.Count > 0 + }; + + new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), + TextManager.Get("editor.paste"), font: GUI.SmallFont) + { + UserData = "paste", + Enabled = MapEntity.CopiedList.Any(), + }; + + new GUITextBlock(new RectTransform(Point.Zero, contextMenu.Content.RectTransform), + TextManager.Get("delete"), font: GUI.SmallFont) + { + UserData = "delete", + Enabled = targets.Count > 0 + }; } - contextMenu.Content.Children.ForEach(c => c.RectTransform.MinSize = new Point(0, c.Rect.Height)); - contextMenu.RectTransform.NonScaledSize = new Point( - contextMenu.Rect.Width, - (int)((contextMenu.Content.CountChildren * 20) * GUI.Scale)); + foreach (var guiComponent in contextMenu.Content.Children) + { + if (guiComponent is GUITextBlock child) + { + if (!child.Enabled) + { + child.TextColor *= 0.5f; + } + } + } + + contextMenu.Content.Children.ForEach(c => + { + if (c is GUITextBlock block) + { + block.RectTransform.NonScaledSize = new Point((int) (block.TextSize.X + block.Padding.X * 2), (int)(18 * GUI.Scale)); + } + }); + int biggestSize = contextMenu.Content.Children.Max(c => c.Rect.Width + (int)contextMenu.Padding.X * 2); + contextMenu.Content.Children.ForEach(c => c.RectTransform.MinSize = new Point(biggestSize, c.Rect.Height)); + contextMenu.RectTransform.NonScaledSize = new Point(biggestSize, (int)(contextMenu.Content.Children.Sum(c => c.Rect.Height) + (contextMenu.Padding.X * 2))); - contextMenu.OnSelected = (GUIComponent component, object obj) => + contextMenu.OnSelected = (component, obj) => { if (!component.Enabled) { return false; } switch (obj as string) { + case "bgcolor": + CreateBackgroundColorPicker(); + break; case "copy": MapEntity.Copy(targets); break; @@ -1878,7 +2246,10 @@ namespace Barotrauma MapEntity.Paste(cam.ScreenToWorld(contextMenu.Rect.Location.ToVector2())); break; case "delete": - targets.ForEach(me => me.Remove()); + targets.ForEach(me => { me.Remove(); }); + break; + case "open" when target != null: + OpenItem(target); break; } contextMenu = null; @@ -1886,6 +2257,57 @@ namespace Barotrauma }; } + /// + /// Creates a color picker that can be used to change the submarine editor's background color + /// + private void CreateBackgroundColorPicker() + { + var msgBox = new GUIMessageBox(TextManager.Get("CharacterEditor.EditBackgroundColor"), "", new[] { TextManager.Get("Reset"), TextManager.Get("OK")}, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175)); + + var rgbLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.25f), msgBox.Content.RectTransform), isHorizontal: true); + + // Generate R,G,B labels and parent elements + var layoutParents = new GUILayoutGroup[3]; + for (int i = 0; i < 3; i++) + { + var colorContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.33f, 1), rgbLayout.RectTransform), isHorizontal: true) { Stretch = true }; + new GUITextBlock(new RectTransform(new Vector2(0.2f, 1), colorContainer.RectTransform, Anchor.CenterLeft) { MinSize = new Point(15, 0) }, GUI.colorComponentLabels[i], font: GUI.SmallFont, textAlignment: Alignment.Center); + layoutParents[i] = colorContainer; + } + + // attach number inputs to our generated parent elements + var rInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1f), layoutParents[0].RectTransform), GUINumberInput.NumberType.Int) { IntValue = backgroundColor.R }; + var gInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1f), layoutParents[1].RectTransform), GUINumberInput.NumberType.Int) { IntValue = backgroundColor.G }; + var bInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1f), layoutParents[2].RectTransform), GUINumberInput.NumberType.Int) { IntValue = backgroundColor.B }; + + rInput.MinValueInt = gInput.MinValueInt = bInput.MinValueInt = 0; + rInput.MaxValueInt = gInput.MaxValueInt = bInput.MaxValueInt = 255; + + rInput.OnValueChanged = gInput.OnValueChanged = bInput.OnValueChanged = delegate + { + var color = new Color(rInput.IntValue, gInput.IntValue, bInput.IntValue); + backgroundColor = color; + GameSettings.SubEditorBackgroundColor = color; + }; + + // Reset button + msgBox.Buttons[0].OnClicked = (button, o) => + { + rInput.IntValue = 13; + gInput.IntValue = 37; + bInput.IntValue = 69; + return true; + }; + + // Ok button + msgBox.Buttons[1].OnClicked = (button, o) => + { + msgBox.Close(); + GameMain.Config.SaveNewPlayerConfig(); + return true; + }; + } + private GUIFrame CreateWiringPanel() { GUIFrame frame = new GUIFrame(new RectTransform(new Vector2(0.03f, 0.35f), GUI.Canvas) @@ -1918,7 +2340,7 @@ namespace Barotrauma private bool SelectLinkedSub(GUIComponent selected, object userData) { - if (!(selected.UserData is Submarine submarine)) return false; + if (!(selected.UserData is SubmarineInfo submarine)) return false; var prefab = new LinkedSubmarinePrefab(submarine); MapEntityPrefab.SelectPrefab(prefab); return true; @@ -1955,6 +2377,73 @@ namespace Barotrauma } + /// + /// Tries to open an item container in the submarine editor using the dummy character + /// + /// The item we want to open + private void OpenItem(Item itemContainer) + { + if (dummyCharacter == null || itemContainer == null) { return; } + + if ((itemContainer.GetComponent() != null || itemContainer.GetComponent() != null) && itemContainer.GetComponent() != null) + { + // We teleport our dummy character to the item so it appears as the entity stays still when in reality the dummy is holding it + oldItemPosition = itemContainer.SimPosition; + TeleportDummyCharacter(oldItemPosition); + + // Override this so we can be sure the container opens + var container = itemContainer.GetComponent(); + if (container != null) { container.KeepOpenWhenEquipped = true; } + + // We accept any slots except "Any" since that would take priority + List allowedSlots = new List(); + itemContainer.AllowedSlots.ForEach(type => + { + if (type != InvSlotType.Any) { allowedSlots.Add(type); } + }); + + // Try to place the item in the dummy character's inventory + bool success = dummyCharacter.Inventory.TryPutItem(itemContainer, dummyCharacter, allowedSlots); + if (success) { OpenedItem = itemContainer; } + else { return; } + } + MapEntity.SelectedList.Clear(); + MapEntity.FilteredSelectedList.Clear(); + MapEntity.SelectEntity(itemContainer); + dummyCharacter.SelectedConstruction = itemContainer; + FilterEntities(entityFilterBox.Text); + } + + /// + /// Close the currently opened item + /// + private void CloseItem() + { + if (dummyCharacter == null) { return; } + DraggedItemPrefab = null; + dummyCharacter.SelectedConstruction = null; + OpenedItem?.Drop(dummyCharacter); + OpenedItem?.SetTransform(oldItemPosition, 0f); + OpenedItem = null; + FilterEntities(entityFilterBox.Text); + } + + /// + /// Teleports the dummy character to the specified position + /// + /// The desired position + private void TeleportDummyCharacter(Vector2 pos) + { + if (dummyCharacter != null) + { + foreach (Limb limb in dummyCharacter.AnimController.Limbs) + { + limb.body.SetTransform(pos, 0.0f); + } + dummyCharacter.AnimController.Collider.SetTransform(pos, 0); + } + } + private bool ChangeSubName(GUITextBox textBox, string text) { if (string.IsNullOrWhiteSpace(text)) @@ -1963,7 +2452,7 @@ namespace Barotrauma return false; } - if (Submarine.MainSub != null) Submarine.MainSub.Name = text; + if (Submarine.MainSub != null) Submarine.MainSub.Info.Name = text; textBox.Deselect(); textBox.Text = text; @@ -1973,11 +2462,11 @@ namespace Barotrauma return true; } - private bool ChangeSubDescription(GUITextBox textBox, string text) + private void ChangeSubDescription(GUITextBox textBox, string text) { if (Submarine.MainSub != null) { - Submarine.MainSub.Description = text; + Submarine.MainSub.Info.Description = text; } else { @@ -1985,8 +2474,26 @@ namespace Barotrauma } submarineDescriptionCharacterCount.Text = text.Length + " / " + submarineDescriptionLimit; + } - return true; + /// + /// Checks if the prefab is an item or if it only consists of items + /// + /// The prefab to check + /// True if the the prefab is an item or it contains only items + private bool IsItemPrefab(MapEntityPrefab mapPrefab) + { + if (dummyCharacter?.SelectedConstruction == null) + { + return false; + } + + return mapPrefab switch + { + ItemPrefab iPrefab => true, + ItemAssemblyPrefab aPrefab => aPrefab.DisplayEntities.All(pair => pair.First is ItemPrefab), + _ => false + }; } private bool SelectPrefab(GUIComponent component, object obj) @@ -1994,7 +2501,7 @@ namespace Barotrauma if (GUI.MouseOn is GUIButton || GUI.MouseOn?.Parent is GUIButton) { return false; } AddPreviouslyUsed(obj as MapEntityPrefab); - + //if selecting a gap/hull/waypoint/spawnpoint, make sure the visibility is toggled on if (obj is CoreEntityPrefab prefab) { @@ -2003,22 +2510,81 @@ namespace Barotrauma { previouslyUsedPanel.Visible = false; showEntitiesPanel.Visible = true; + showEntitiesPanel.RectTransform.AbsoluteOffset = new Point(Math.Max(entityCountPanel.Rect.Right, saveAssemblyFrame.Rect.Right), TopPanel.Rect.Height); matchingTickBox.Selected = true; matchingTickBox.Flash(GUI.Style.Green); } } - MapEntityPrefab.SelectPrefab(obj); - GUI.ForceMouseOn(null); + if (dummyCharacter?.SelectedConstruction != null) + { + var inv = dummyCharacter?.SelectedConstruction?.OwnInventory; + if (inv != null) + { + switch (obj) + { + case ItemAssemblyPrefab assemblyPrefab when PlayerInput.IsShiftDown(): + { + var itemInstance = LoadItemAssemblyInventorySafe(assemblyPrefab); + var spawnedItem = false; + + itemInstance.ForEach(newItem => + { + if (newItem != null) + { + var placedItem = inv.TryPutItem(newItem, dummyCharacter); + spawnedItem |= placedItem; + + if (!placedItem) + { + // Remove everything inside of the item so we don't get the popup asking if we want to keep the contained items + newItem.OwnInventory?.DeleteAllItems(); + newItem.Remove(); + } + } + }); + GUI.PlayUISound(spawnedItem ? GUISoundType.PickItem : GUISoundType.PickItemFail); + break; + } + case ItemPrefab itemPrefab when PlayerInput.IsShiftDown(): + { + var item = new Item(itemPrefab, Vector2.Zero, Submarine.MainSub); + if (!inv.TryPutItem(item, dummyCharacter)) + { + // We failed, remove the item so it doesn't stay at x:0,y:0 + GUI.PlayUISound(GUISoundType.PickItemFail); + item.Remove(); + } + else + { + GUI.PlayUISound(GUISoundType.PickItem); + } + break; + } + case ItemAssemblyPrefab _: + case ItemPrefab _: + { + // Place the item into our hands + DraggedItemPrefab = (MapEntityPrefab) obj; + GUI.PlayUISound(GUISoundType.PickItem); + break; + } + } + } + } + else + { + GUI.PlayUISound(GUISoundType.PickItem); + MapEntityPrefab.SelectPrefab(obj); + } + return false; } - private bool GenerateWaypoints(GUIButton button, object obj) + private bool GenerateWaypoints() { - if (Submarine.MainSub == null) return false; - - WayPoint.GenerateSubWaypoints(Submarine.MainSub); - return true; + if (Submarine.MainSub == null) { return false; } + return WayPoint.GenerateSubWaypoints(Submarine.MainSub); } private void AddPreviouslyUsed(MapEntityPrefab mapEntityPrefab) @@ -2056,21 +2622,19 @@ namespace Barotrauma } List wallPoints = new List(); - Vector2 min = Vector2.Zero; - Vector2 max = Vector2.Zero; + Vector2 max; List mapEntityList = new List(); foreach (MapEntity e in MapEntity.mapEntityList) { - if (e is Item) + if (e is Item it) { - Item it = e as Item; Door door = it.GetComponent(); if (door != null) { - int halfW = e.WorldRect.Width / 2; - wallPoints.Add(new Vector2(e.WorldRect.X + halfW, -e.WorldRect.Y + e.WorldRect.Height)); + int halfW = it.WorldRect.Width / 2; + wallPoints.Add(new Vector2(it.WorldRect.X + halfW, -it.WorldRect.Y + it.WorldRect.Height)); mapEntityList.Add(it); } continue; @@ -2101,7 +2665,7 @@ namespace Barotrauma return; } - min = wallPoints[0]; + var min = wallPoints[0]; max = wallPoints[0]; for (int i = 0; i < wallPoints.Count; i++) { @@ -2344,8 +2908,7 @@ namespace Barotrauma Rectangle gapRect = e.WorldRect; gapRect.Y -= 8; gapRect.Height = 16; - Gap newGap = new Gap(MapEntityPrefab.Find(null, "gap"), - gapRect); + Gap newGap = new Gap(MapEntityPrefab.Find(null, "gap"), gapRect); } } @@ -2372,29 +2935,42 @@ namespace Barotrauma MapEntity.HighlightedListBox.AddToGUIUpdateList(); } - if ((CharacterMode || WiringMode) && dummyCharacter != null) + if (dummyCharacter != null) { CharacterHUD.AddToGUIUpdateList(dummyCharacter); if (dummyCharacter.SelectedConstruction != null) { dummyCharacter.SelectedConstruction.AddToGUIUpdateList(); } - else if (WiringMode && MapEntity.SelectedList.Count == 1 && MapEntity.SelectedList[0] is Item item && item.GetComponent() != null) + else if (WiringMode && MapEntity.SelectedList.FirstOrDefault() is Item item && item.GetComponent() != null) { - MapEntity.SelectedList[0].AddToGUIUpdateList(); + MapEntity.SelectedList.FirstOrDefault()?.AddToGUIUpdateList(); } } - else + if (loadFrame != null) { - if (loadFrame != null) - { - loadFrame.AddToGUIUpdateList(); - } - else if (saveFrame != null) - { - saveFrame.AddToGUIUpdateList(); - } + loadFrame.AddToGUIUpdateList(); } + else if (saveFrame != null) + { + saveFrame.AddToGUIUpdateList(); + } + } + + /// + /// GUI.MouseOn doesn't get updated while holding primary mouse and we need it to + /// + private bool IsMouseOnEditorGUI() + { + if (GUI.MouseOn == null) + { + return false; + } + + return (EntityMenu?.MouseRect.Contains(PlayerInput.MousePosition) ?? false) + || (entityCountPanel?.MouseRect.Contains(PlayerInput.MousePosition) ?? false) + || (MapEntity.EditingHUD?.MouseRect.Contains(PlayerInput.MousePosition) ?? false) + || (TopPanel?.MouseRect.Contains(PlayerInput.MousePosition) ?? false); } /// @@ -2412,20 +2988,193 @@ namespace Barotrauma UpdateEntityList(); } + if (WiringMode && dummyCharacter != null) + { + Wire equippedWire = + Character.Controlled?.SelectedItems[0]?.GetComponent() ?? + Character.Controlled?.SelectedItems[1]?.GetComponent() ?? + Wire.DraggingWire; + + if (equippedWire == null) + { + // Highlight wires when hovering over the entity selection box + if (MapEntity.HighlightedListBox != null) + { + var lBox = MapEntity.HighlightedListBox; + foreach (var child in lBox.Content.Children) + { + if (child.UserData is Item item) + { + item.ExternalHighlight = GUI.IsMouseOn(child); + } + } + } + + var highlightedEntities = new List(); + + // ReSharper disable once LoopCanBeConvertedToQuery + foreach (Item item in MapEntity.mapEntityList.Where(entity => entity is Item).Cast()) + { + var wire = item.GetComponent(); + if (wire == null || !wire.IsMouseOn()) { continue; } + highlightedEntities.Add(item); + } + + MapEntity.UpdateHighlighting(highlightedEntities, true); + } + } + hullVolumeFrame.Visible = MapEntity.SelectedList.Any(s => s is Hull); saveAssemblyFrame.Visible = MapEntity.SelectedList.Count > 0; - if (PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Tab)) + var offset = cam.WorldView.Top - cam.ScreenToWorld(new Vector2(0, GameMain.GraphicsHeight - EntityMenu.Rect.Top)).Y; + + // Move the camera towards to the focus point + if (camTargetFocus != Vector2.Zero) { - entityFilterBox.Select(); + if (GameMain.Config.KeyBind(InputType.Up).IsDown() || GameMain.Config.KeyBind(InputType.Down).IsDown() || + GameMain.Config.KeyBind(InputType.Left).IsDown() || GameMain.Config.KeyBind(InputType.Right).IsDown()) + { + camTargetFocus = Vector2.Zero; + } + else + { + var targetWithOffset = new Vector2(camTargetFocus.X, camTargetFocus.Y - offset / 2); + if (Math.Abs(cam.Position.X - targetWithOffset.X) < 1.0f && + Math.Abs(cam.Position.Y - targetWithOffset.Y) < 1.0f) + { + camTargetFocus = Vector2.Zero; + } + else + { + cam.Position += (targetWithOffset - cam.Position) / cam.MoveSmoothness; + } + } } - cam.MoveCamera((float)deltaTime, true); + + if (GUI.KeyboardDispatcher.Subscriber == null) + { + if (PlayerInput.KeyHit(Keys.E) && mode == Mode.Default) + { + if (dummyCharacter != null) + { + if (dummyCharacter.SelectedConstruction == null) + { + foreach (var entity in MapEntity.mapEntityList) + { + if (entity is Item item && entity.IsHighlighted && item.Components.Any(ic => !(ic is ConnectionPanel) && !(ic is Repairable) && ic.GuiFrame != null)) + { + var container = item.GetComponents().ToList(); + if (!container.Any() || container.Any(ic => ic?.DrawInventory ?? false)) + { + OpenItem(item); + break; + } + } + } + } + else + { + CloseItem(); + } + } + } + + // Focus to selection + if (PlayerInput.KeyHit(Keys.F) && mode == Mode.Default) + { + // content warning: contains coordinate system workarounds + var selected = MapEntity.SelectedList; + if (selected.Count > 0) + { + var dRect = selected.First().Rect; + var rect = new Rectangle(dRect.Left, dRect.Top, dRect.Width, dRect.Height * -1); + if (selected.Count > 1) + { + // Create one big rect out of our selection + selected.Skip(1).ForEach(me => + { + var wRect = me.Rect; + rect = Rectangle.Union(rect, new Rectangle(wRect.Left, wRect.Top, wRect.Width, wRect.Height * -1)); + }); + } + camTargetFocus = rect.Center.ToVector2(); + } + } + + // TODO adjust when the new inventory stuff rolls in + if (PlayerInput.KeyHit(Keys.Q) && mode == Mode.Default) + { + toggleEntityMenuButton.OnClicked?.Invoke(toggleEntityMenuButton, toggleEntityMenuButton.UserData); + } + + if (PlayerInput.KeyHit(Keys.Tab)) + { + entityFilterBox.Select(); + } + + if (PlayerInput.IsCtrlDown() && MapEntity.StartMovingPos == Vector2.Zero) + { + cam.MoveCamera((float) deltaTime, allowMove: false); + // Save menu + if (PlayerInput.KeyHit(Keys.S)) + { + if (PlayerInput.IsShiftDown()) + { + // Quick-save, but only when we've set a custom name for our sub + CreateSaveScreen(subNameLabel != null && subNameLabel.Text != TextManager.Get("unspecifiedsubfilename")); + } + else + { + // Save menu + CreateSaveScreen(); + } + } + + // Select or deselect everything + if (PlayerInput.KeyHit(Keys.A) && mode == Mode.Default) + { + if (MapEntity.SelectedList.Any()) + { + MapEntity.DeselectAll(); + } + else + { + var selectables = MapEntity.mapEntityList.Where(entity => entity.SelectableInEditor).ToList(); + lock (selectables) + { + selectables.ForEach(MapEntity.AddSelection); + } + } + } + + // 1-2 keys on the keyboard for switching modes + if (PlayerInput.KeyHit(Keys.D1)) { SetMode(Mode.Default); } + if (PlayerInput.KeyHit(Keys.D2)) { SetMode(Mode.Wiring); } + } + else + { + cam.MoveCamera((float) deltaTime, allowMove: true); + } + } + else + { + cam.MoveCamera((float) deltaTime, allowMove: false); + } + if (PlayerInput.MidButtonHeld()) { - Vector2 moveSpeed = PlayerInput.MouseSpeed * (float)deltaTime * 100.0f / cam.Zoom; + Vector2 moveSpeed = PlayerInput.MouseSpeed * (float)deltaTime * 60.0f / cam.Zoom; moveSpeed.X = -moveSpeed.X; cam.Position += moveSpeed; + // break out of trying to focus + camTargetFocus = Vector2.Zero; + } + + if (PlayerInput.KeyHit(Keys.Escape) && dummyCharacter != null) + { + CloseItem(); } if (contextMenu != null) @@ -2438,20 +3187,16 @@ namespace Barotrauma } } - if (CharacterMode || WiringMode) + if (dummyCharacter != null && Entity.FindEntityByID(dummyCharacter.ID) == dummyCharacter) { - if (dummyCharacter == null || Entity.FindEntityByID(dummyCharacter.ID) != dummyCharacter) - { - SetMode(Mode.Default); - } - else + if (WiringMode) { foreach (MapEntity me in MapEntity.mapEntityList) { me.IsHighlighted = false; } - if (WiringMode && dummyCharacter.SelectedConstruction == null) + if (dummyCharacter.SelectedConstruction == null) { List wires = new List(); foreach (Item item in Item.ItemList) @@ -2461,68 +3206,251 @@ namespace Barotrauma } Wire.UpdateEditing(wires); } + } - if (dummyCharacter.SelectedConstruction == null || - dummyCharacter.SelectedConstruction.GetComponent() != null) + if (!WiringMode) + { + // Move all of our slots on top center of the entity list + // We use the slots to open item inventories and we want the position of them to be consisent + dummyCharacter.Inventory.slots.ForEach(slot => { - if (WiringMode && (PlayerInput.KeyDown(Microsoft.Xna.Framework.Input.Keys.LeftShift) || PlayerInput.KeyDown(Microsoft.Xna.Framework.Input.Keys.Right))) + slot.Rect.Y = EntityMenu.Rect.Top; + slot.Rect.X = EntityMenu.Rect.X + (EntityMenu.Rect.Width / 2) - (slot.Rect.Width /2); + }); + } + + if (dummyCharacter.SelectedConstruction == null || + dummyCharacter.SelectedConstruction.GetComponent() != null) + { + if (WiringMode && PlayerInput.IsShiftDown()) + { + Wire equippedWire = + Character.Controlled?.SelectedItems[0]?.GetComponent() ?? + Character.Controlled?.SelectedItems[1]?.GetComponent(); + if (equippedWire != null && equippedWire.GetNodes().Count > 0) { - Wire equippedWire = - Character.Controlled?.SelectedItems[0]?.GetComponent() ?? - Character.Controlled?.SelectedItems[1]?.GetComponent(); - if (equippedWire != null && equippedWire.GetNodes().Count > 0) + Vector2 lastNode = equippedWire.GetNodes().Last(); + if (equippedWire.Item.Submarine != null) { - Vector2 lastNode = equippedWire.GetNodes().Last(); - if (equippedWire.Item.Submarine != null) - { - lastNode += equippedWire.Item.Submarine.HiddenSubPosition + equippedWire.Item.Submarine.Position; - } - - dummyCharacter.CursorPosition = - Math.Abs(dummyCharacter.CursorPosition.X - lastNode.X) < Math.Abs(dummyCharacter.CursorPosition.Y - lastNode.Y) ? - new Vector2(lastNode.X, dummyCharacter.CursorPosition.Y) : - dummyCharacter.CursorPosition = new Vector2(dummyCharacter.CursorPosition.X, lastNode.Y); + lastNode += equippedWire.Item.Submarine.HiddenSubPosition + equippedWire.Item.Submarine.Position; } - } - Vector2 mouseSimPos = FarseerPhysics.ConvertUnits.ToSimUnits(dummyCharacter.CursorPosition); - foreach (Limb limb in dummyCharacter.AnimController.Limbs) - { - limb.body.SetTransform(mouseSimPos, 0.0f); + var (cursorX, cursorY) = dummyCharacter.CursorPosition; + + bool isHorizontal = Math.Abs(cursorX - lastNode.X) < Math.Abs(cursorY - lastNode.Y); + + float roundedY = MathUtils.Round(cursorY, Submarine.GridSize.Y / 2.0f); + float roundedX = MathUtils.Round(cursorX, Submarine.GridSize.X / 2.0f); + + dummyCharacter.CursorPosition = isHorizontal + ? new Vector2(lastNode.X, roundedY) + : new Vector2(roundedX, lastNode.Y); } - dummyCharacter.AnimController.Collider.SetTransform(mouseSimPos, 0.0f); } + // Keep teleporting the dummy character to the opened item to make it look like the container didn't go anywhere + if (OpenedItem != null) + { + TeleportDummyCharacter(oldItemPosition); + } + + if (WiringMode && dummyCharacter?.SelectedConstruction == null) + { + TeleportDummyCharacter(FarseerPhysics.ConvertUnits.ToSimUnits(dummyCharacter.CursorPosition)); + } + } + + if (WiringMode) + { dummyCharacter.ControlLocalPlayer((float)deltaTime, cam, false); dummyCharacter.Control((float)deltaTime, cam); + } - dummyCharacter.Submarine = Submarine.MainSub; + cam.TargetPos = Vector2.Zero; + dummyCharacter.Submarine = Submarine.MainSub; + } - cam.TargetPos = Vector2.Zero; + // Deposit item from our "infinite stack" into inventory slots + var inv = dummyCharacter?.SelectedConstruction?.OwnInventory; + if (inv?.slots != null) + { + var dragginMouse = MouseDragStart != Vector2.Zero && Vector2.Distance(PlayerInput.MousePosition, MouseDragStart) >= GUI.Scale * 20; + + // So we don't accidentally drag inventory items while doing this + if (DraggedItemPrefab != null) { Inventory.draggingItem = null; } + + switch (DraggedItemPrefab) + { + // regular item prefabs + case ItemPrefab itemPrefab when PlayerInput.PrimaryMouseButtonClicked() || dragginMouse: + { + bool spawnedItem = false; + for (var i = 0; i < inv.slots.Length; i++) + { + var slot = inv.slots[i]; + var itemContainer = inv?.Items[i]?.GetComponent(); + + // check if the slot is empty or if we can place the item into a container, for example an oxygen tank into a diving suit + if (Inventory.IsMouseOnSlot(slot)) + { + var newItem = new Item(itemPrefab, Vector2.Zero, Submarine.MainSub); + + if (inv.Items[i] == null) + { + bool placedItem = inv.TryPutItem(newItem, i, false, true, dummyCharacter); + spawnedItem |= placedItem; + + if (!placedItem) + { + newItem.Remove(); + } + } + else if (itemContainer != null && itemContainer.CanBeContained(itemPrefab) && + (itemContainer.Inventory?.Items.Any(item => item == null) ?? false)) + { + bool placedItem = itemContainer.Inventory.TryPutItem(newItem, dummyCharacter); + spawnedItem |= placedItem; + + // try to place the item into the inventory of the item we are hovering over + if (!placedItem) + { + newItem.Remove(); + } + else + { + slot.ShowBorderHighlight(GUI.Style.Green, 0.1f, 0.4f); + } + } + else + { + newItem.Remove(); + } + + if (!dragginMouse) + { + GUI.PlayUISound(spawnedItem ? GUISoundType.PickItem : GUISoundType.PickItemFail); + } + } + } + break; + } + // item assemblies + case ItemAssemblyPrefab assemblyPrefab when PlayerInput.PrimaryMouseButtonClicked(): + { + bool spawnedItems = false; + for (var i = 0; i < inv.slots.Length; i++) + { + var slot = inv.slots[i]; + var itemContainer = inv?.Items[i]?.GetComponent(); + if (inv.Items[i] == null && Inventory.IsMouseOnSlot(slot)) + { + // load the items + var itemInstance = LoadItemAssemblyInventorySafe(assemblyPrefab); + + // counter for items that failed so we so we known that slot remained empty + var failedCount = 0; + + for (var j = 0; j < itemInstance.Count(); j++) + { + var newItem = itemInstance[j]; + var newSpot = i + j - failedCount; + + // try to find a valid slot to put the items + while (inv.slots.Length > newSpot) + { + if (inv.Items[newSpot] == null) { break; } + newSpot++; + } + + // valid slot found + if (inv.slots.Length > newSpot) + { + var placedItem = inv.TryPutItem(newItem, newSpot, false, true, dummyCharacter); + spawnedItems |= placedItem; + + if (!placedItem) + { + failedCount++; + // delete the included items too so we don't get a popup asking if we want to keep them + newItem?.OwnInventory?.DeleteAllItems(); + newItem.Remove(); + } + } + else + { + var placedItem = inv.TryPutItem(newItem, dummyCharacter); + spawnedItems |= placedItem; + + // if our while loop didn't find a valid slot then let the inventory decide where to put it as a last resort + if (!placedItem) + { + // delete the included items too so we don't get a popup asking if we want to keep them + newItem?.OwnInventory?.DeleteAllItems(); + newItem.Remove(); + } + } + } + } + } + GUI.PlayUISound(spawnedItems ? GUISoundType.PickItem : GUISoundType.PickItemFail); + break; + } } } - else if (!saveAssemblyFrame.Rect.Contains(PlayerInput.MousePosition)) + + // Update our mouse dragging state so we can easily slide thru slots while holding the mouse button down to place lots of items + if (PlayerInput.PrimaryMouseButtonHeld()) + { + if (MouseDragStart == Vector2.Zero) + { + MouseDragStart = PlayerInput.MousePosition; + } + } + else + { + MouseDragStart = Vector2.Zero; + } + + if (!saveAssemblyFrame.Rect.Contains(PlayerInput.MousePosition) && dummyCharacter?.SelectedConstruction == null && !WiringMode && GUI.MouseOn == null) { MapEntity.UpdateSelecting(cam); } - - if (!CharacterMode && !WiringMode) + + if (!WiringMode) { + bool shouldCloseHud = dummyCharacter?.SelectedConstruction != null && HUD.CloseHUD(dummyCharacter.SelectedConstruction.Rect) && DraggedItemPrefab == null; + if (MapEntityPrefab.Selected != null && GUI.MouseOn == null) { MapEntityPrefab.Selected.UpdatePlacing(cam); } else { - if (PlayerInput.RightButtonClicked()) + if (PlayerInput.SecondaryMouseButtonClicked() && !shouldCloseHud) { - CreateContextMenu(); + if (GUI.IsMouseOn(entityFilterBox)) + { + ClearFilter(); + } + else + { + if (dummyCharacter?.SelectedConstruction == null) + { + CreateContextMenu(); + } + DraggedItemPrefab = null; + } + } + + if (shouldCloseHud) + { + CloseItem(); } } MapEntity.UpdateEditor(cam); } - entityMenuOpenState = entityMenuOpen && !CharacterMode & !WiringMode ? + entityMenuOpenState = entityMenuOpen && !WiringMode ? (float)Math.Min(entityMenuOpenState + deltaTime * 5.0f, 1.0f) : (float)Math.Max(entityMenuOpenState - deltaTime * 5.0f, 0.0f); @@ -2556,7 +3484,7 @@ namespace Barotrauma } } - if ((CharacterMode || WiringMode) && dummyCharacter != null) + if (dummyCharacter != null) { dummyCharacter.AnimController.FindHull(dummyCharacter.CursorWorldPosition, false); @@ -2570,34 +3498,29 @@ namespace Barotrauma //wires need to be updated for the last node to follow the player during rewiring Wire wire = item.GetComponent(); - if (wire != null) wire.Update((float)deltaTime, cam); + wire?.Update((float)deltaTime, cam); } if (dummyCharacter.SelectedConstruction != null) { - if (dummyCharacter.SelectedConstruction != null) + if (MapEntity.SelectedList.Contains(dummyCharacter.SelectedConstruction) || WiringMode) { - dummyCharacter.SelectedConstruction.UpdateHUD(cam, dummyCharacter, (float)deltaTime); + dummyCharacter.SelectedConstruction?.UpdateHUD(cam, dummyCharacter, (float)deltaTime); } - - //if (PlayerInput.KeyHit(InputType.Select) && dummyCharacter.FocusedItem != dummyCharacter.SelectedConstruction && GUI.KeyboardDispatcher.Subscriber == null) - //{ - // dummyCharacter.SelectedConstruction = null; - //} - /*if (PlayerInput.KeyHit(InputType.Deselect)) + else { - dummyCharacter.SelectedConstruction = null; - }*/ + // We somehow managed to unfocus the item, close it so our framerate doesn't go to 5 because the + // UpdateHUD() method keeps re-creating the editing HUD + CloseItem(); + } } - else if (MapEntity.SelectedList.Count == 1) + else if (MapEntity.SelectedList.Count == 1 && WiringMode) { (MapEntity.SelectedList[0] as Item)?.UpdateHUD(cam, dummyCharacter, (float)deltaTime); } CharacterHUD.Update((float)deltaTime, dummyCharacter, cam); } - - //GUI.Update((float)deltaTime); } /// @@ -2617,26 +3540,44 @@ namespace Barotrauma } spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, transformMatrix: cam.Transform); - graphics.Clear(new Color(0.051f, 0.149f, 0.271f, 1.0f)); + graphics.Clear(backgroundColor); if (GameMain.DebugDraw) { GUI.DrawLine(spriteBatch, new Vector2(Submarine.MainSub.HiddenSubPosition.X, -cam.WorldView.Y), new Vector2(Submarine.MainSub.HiddenSubPosition.X, -(cam.WorldView.Y - cam.WorldView.Height)), Color.White * 0.5f, 1.0f, (int)(2.0f / cam.Zoom)); GUI.DrawLine(spriteBatch, new Vector2(cam.WorldView.X, -Submarine.MainSub.HiddenSubPosition.Y), new Vector2(cam.WorldView.Right, -Submarine.MainSub.HiddenSubPosition.Y), Color.White * 0.5f, 1.0f, (int)(2.0f / cam.Zoom)); } - Submarine.DrawBack(spriteBatch, true, e => e is Structure s && (e.SpriteDepth >= 0.9f || s.Prefab.BackgroundSprite != null)); + Submarine.DrawBack(spriteBatch, true, e => + e is Structure s && + (showThalamus || !s.prefab.Category.HasFlag(MapEntityCategory.Thalamus)) && + (e.SpriteDepth >= 0.9f || s.Prefab.BackgroundSprite != null)); spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, transformMatrix: cam.Transform); - Submarine.DrawBack(spriteBatch, true, e => !(e is Structure) || e.SpriteDepth < 0.9f); + + // When we "open" a wearable item with inventory it won't get rendered because the dummy character is invisible + // So we are drawing a clone of it on the same position + if (OpenedItem?.GetComponent() != null) + { + OpenedItem.Sprite.Draw(spriteBatch, new Vector2(OpenedItem.DrawPosition.X, -(OpenedItem.DrawPosition.Y)), + scale: OpenedItem.Scale, color: OpenedItem.SpriteColor, depth: OpenedItem.SpriteDepth); + GUI.DrawRectangle(spriteBatch, + new Vector2(OpenedItem.WorldRect.X, -OpenedItem.WorldRect.Y), + new Vector2(OpenedItem.Rect.Width, OpenedItem.Rect.Height), + Color.White, false, 0, (int)Math.Max(2.0f / cam.Zoom, 1.0f)); + } + + Submarine.DrawBack(spriteBatch, true, e => + (!(e is Structure) || e.SpriteDepth < 0.9f) && + (showThalamus || !e.prefab.Category.HasFlag(MapEntityCategory.Thalamus))); spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, transformMatrix: cam.Transform); - Submarine.DrawDamageable(spriteBatch, null, editing: true); + Submarine.DrawDamageable(spriteBatch, null, editing: true, e => showThalamus || !(e.prefab?.Category.HasFlag(MapEntityCategory.Thalamus) ?? false)); spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, transformMatrix: cam.Transform); - Submarine.DrawFront(spriteBatch, editing: true); - if (!CharacterMode && !WiringMode && GUI.MouseOn == null) + Submarine.DrawFront(spriteBatch, editing: true, e => showThalamus || !(e.prefab?.Category.HasFlag(MapEntityCategory.Thalamus) ?? false)); + if (!WiringMode && !IsMouseOnEditorGUI()) { MapEntityPrefab.Selected?.DrawPlacing(spriteBatch, cam); MapEntity.DrawSelecting(spriteBatch, cam); @@ -2645,7 +3586,7 @@ namespace Barotrauma if (GameMain.LightManager.LightingEnabled && lightingEnabled) { - spriteBatch.Begin(SpriteSortMode.Deferred, Lights.CustomBlendStates.Multiplicative, null, DepthStencilState.None, null, null, null); + spriteBatch.Begin(SpriteSortMode.Deferred, Lights.CustomBlendStates.Multiplicative, null, DepthStencilState.None); spriteBatch.Draw(GameMain.LightManager.LightMap, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), Color.White); spriteBatch.End(); } @@ -2656,25 +3597,42 @@ namespace Barotrauma if (Submarine.MainSub != null) { + Vector2 position = Submarine.MainSub.SubBody != null ? Submarine.MainSub.WorldPosition : Submarine.MainSub.HiddenSubPosition; + GUI.DrawIndicator( - spriteBatch, Submarine.MainSub.WorldPosition, cam, + spriteBatch, position, cam, cam.WorldView.Width, GUI.SubmarineIcon, Color.LightBlue * 0.5f); } + + var notificationIcon = GUI.Style.GetComponentStyle("GUINotificationButton"); + var tooltipStyle = GUI.Style.GetComponentStyle("GUIToolTip"); + foreach (Gap gap in Gap.GapList) + { + if (gap.linkedTo.Count == 2 && gap.linkedTo[0] == gap.linkedTo[1]) + { + Vector2 screenPos = Cam.WorldToScreen(gap.WorldPosition); + Rectangle rect = new Rectangle(screenPos.ToPoint() - new Point(20), new Point(40)); + tooltipStyle.Sprites[GUIComponent.ComponentState.None][0].Draw(spriteBatch, rect, Color.White); + notificationIcon.Sprites[GUIComponent.ComponentState.None][0].Draw(spriteBatch, rect, GUI.Style.Orange); + if (Vector2.Distance(PlayerInput.MousePosition, screenPos) < 30 * Cam.Zoom) + { + GUIComponent.DrawToolTip(spriteBatch, TextManager.Get("gapinsidehullwarning"), new Rectangle(screenPos.ToPoint(), new Point(10))); + } + } + } - if ((CharacterMode || WiringMode) && dummyCharacter != null) + if (dummyCharacter != null) { - dummyCharacter.DrawHUD(spriteBatch, cam, false); - if (WiringMode) wiringToolPanel.DrawManually(spriteBatch); - } - else - { - MapEntity.DrawEditor(spriteBatch, cam); + if (WiringMode) + { + dummyCharacter.DrawHUD(spriteBatch, cam, false); + wiringToolPanel.DrawManually(spriteBatch); + } } + MapEntity.DrawEditor(spriteBatch, cam); GUI.Draw(Cam, spriteBatch); - - if (!PlayerInput.PrimaryMouseButtonHeld()) Inventory.draggingItem = null; spriteBatch.End(); } @@ -2714,7 +3672,7 @@ namespace Barotrauma }*/ spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, null, null, null, null, transform); - Submarine.Draw(spriteBatch, false); + Submarine.Draw(spriteBatch); Submarine.DrawFront(spriteBatch); Submarine.DrawDamageable(spriteBatch, null); spriteBatch.End(); @@ -2736,6 +3694,6 @@ namespace Barotrauma stream.Dispose(); } - + public static bool IsSubEditor() { return Screen.Selected is SubEditorScreen && !Submarine.Unloading; } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index 5cf2e0892..26f301498 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -399,7 +399,7 @@ namespace Barotrauma ToolTip = toolTip, OnSelected = (tickBox) => { - if (property.TrySetValue(entity, tickBox.Selected)) + if (SetPropertyValue(property, entity, tickBox.Selected)) { TrySendNetworkUpdate(entity, property); } @@ -440,7 +440,7 @@ namespace Barotrauma numberInput.IntValue = value; numberInput.OnValueChanged += (numInput) => { - if (property.TrySetValue(entity, numInput.IntValue)) + if (SetPropertyValue(property, entity, numInput.IntValue)) { TrySendNetworkUpdate(entity, property); } @@ -474,7 +474,7 @@ namespace Barotrauma numberInput.OnValueChanged += (numInput) => { - if (property.TrySetValue(entity, numInput.FloatValue)) + if (SetPropertyValue(property, entity, numInput.FloatValue)) { // This causes stack overflow. What's the purpose of it? //numInput.FloatValue = (float)property.GetValue(entity); @@ -504,7 +504,7 @@ namespace Barotrauma enumDropDown.SelectItem(value); enumDropDown.OnSelected += (selected, val) => { - if (property.TrySetValue(entity, val)) + if (SetPropertyValue(property, entity, val)) { TrySendNetworkUpdate(entity, property); } @@ -536,7 +536,7 @@ namespace Barotrauma } enumDropDown.OnSelected += (selected, val) => { - if (property.TrySetValue(entity, string.Join(", ", enumDropDown.SelectedDataMultiple.Select(d => d.ToString())))) + if (SetPropertyValue(property, entity, string.Join(", ", enumDropDown.SelectedDataMultiple.Select(d => d.ToString())))) { TrySendNetworkUpdate(entity, property); } @@ -568,16 +568,23 @@ namespace Barotrauma ToolTip = toolTip, Font = GUI.SmallFont, Text = value, - OverflowClip = true, + OverflowClip = true }; - propertyBox.OnDeselected += (textBox, keys) => + + propertyBox.OnDeselected += (textBox, keys) => OnApply(textBox); + propertyBox.OnEnterPressed += (box, text) => OnApply(box); + + bool OnApply(GUITextBox textBox) { - if (property.TrySetValue(entity, textBox.Text)) + if (SetPropertyValue(property, entity, textBox.Text)) { TrySendNetworkUpdate(entity, property); - textBox.Text = (string)property.GetValue(entity); + textBox.Text = (string) property.GetValue(entity); + textBox.Flash(GUI.Style.Green, flashDuration: 1f); } - }; + return true; + } + if (translationTextTag != null) { new GUIButton(new RectTransform(new Vector2(browseButtonWidth, 1), frame.RectTransform, Anchor.TopRight), "...", style: "GUIButtonSmall") @@ -647,7 +654,7 @@ namespace Barotrauma else newVal.Y = numInput.IntValue; - if (property.TrySetValue(entity, newVal)) + if (SetPropertyValue(property, entity, newVal)) { TrySendNetworkUpdate(entity, property); } @@ -702,7 +709,7 @@ namespace Barotrauma else newVal.Y = numInput.FloatValue; - if (property.TrySetValue(entity, newVal)) + if (SetPropertyValue(property, entity, newVal)) { TrySendNetworkUpdate(entity, property); } @@ -761,7 +768,7 @@ namespace Barotrauma else newVal.Z = numInput.FloatValue; - if (property.TrySetValue(entity, newVal)) + if (SetPropertyValue(property, entity, newVal)) { TrySendNetworkUpdate(entity, property); } @@ -824,7 +831,7 @@ namespace Barotrauma else newVal.W = numInput.FloatValue; - if (property.TrySetValue(entity, newVal)) + if (SetPropertyValue(property, entity, newVal)) { TrySendNetworkUpdate(entity, property); } @@ -894,7 +901,7 @@ namespace Barotrauma else newVal.A = (byte)(numInput.IntValue); - if (property.TrySetValue(entity, newVal)) + if (SetPropertyValue(property, entity, newVal)) { TrySendNetworkUpdate(entity, property); colorBox.Color = newVal; @@ -958,7 +965,7 @@ namespace Barotrauma else newVal.Height = numInput.IntValue; - if (property.TrySetValue(entity, newVal)) + if (SetPropertyValue(property, entity, newVal)) { TrySendNetworkUpdate(entity, property); } @@ -980,7 +987,7 @@ namespace Barotrauma { string text = userData as string ?? ""; - if (property.TrySetValue(entity, text)) + if (SetPropertyValue(property, entity, text)) { TrySendNetworkUpdate(entity, property); textBox.Text = (string)property.GetValue(entity); @@ -1019,6 +1026,61 @@ namespace Barotrauma } } } - } + private bool SetPropertyValue(SerializableProperty property, object entity, object value) + { + MultiSetProperties(property, entity, value); + return property.TrySetValue(entity, value); + } + + /// + /// Sets common shared properties to all selected map entities in sub editor. + /// Only works client side while in the sub editor and when parentObject is ItemComponent, Item or Structure. + /// + /// + /// + /// + /// The function has the same parameters as + private void MultiSetProperties(SerializableProperty property, object parentObject, object value) + { + if (!(Screen.Selected is SubEditorScreen) || MapEntity.SelectedList.Count <= 1) { return; } + if (!(parentObject is ItemComponent || parentObject is Item || parentObject is Structure)) { return; } + + foreach (var entity in MapEntity.SelectedList.Where(entity => entity != parentObject)) + { + switch (parentObject) + { + case Structure _: + case Item _: + { + if (entity.GetType() == parentObject.GetType()) + { + property.PropertyInfo.SetValue(entity, value); + } + else if (entity is ISerializableEntity sEntity && sEntity.SerializableProperties != null) + { + var props = sEntity.SerializableProperties; + + if (props.TryGetValue(property.NameToLowerInvariant, out SerializableProperty foundProp)) + { + foundProp.PropertyInfo.SetValue(entity, value); + } + } + break; + } + case ItemComponent _ when entity is Item item: + { + foreach (var component in item.Components) + { + if (component.GetType() == parentObject.GetType() && component != parentObject) + { + property.PropertyInfo.SetValue(component, value); + } + } + break; + } + } + } + } + } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs index cb28e241c..9c64fa486 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs @@ -97,6 +97,32 @@ namespace Barotrauma.Sounds if (position != null) { + if (float.IsNaN(position.Value.X)) + { + throw new Exception("Failed to set source's position: " + debugName + ", position.X is NaN"); + } + if (float.IsNaN(position.Value.Y)) + { + throw new Exception("Failed to set source's position: " + debugName + ", position.Y is NaN"); + } + if (float.IsNaN(position.Value.Z)) + { + throw new Exception("Failed to set source's position: " + debugName + ", position.Z is NaN"); + } + + if (float.IsInfinity(position.Value.X)) + { + throw new Exception("Failed to set source's position: " + debugName + ", position.X is Infinity"); + } + if (float.IsInfinity(position.Value.Y)) + { + throw new Exception("Failed to set source's position: " + debugName + ", position.Y is Infinity"); + } + if (float.IsInfinity(position.Value.Z)) + { + throw new Exception("Failed to set source's position: " + debugName + ", position.Z is Infinity"); + } + uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); Al.Sourcei(alSource, Al.SourceRelative, Al.False); int alError = Al.GetError(); @@ -179,7 +205,7 @@ namespace Barotrauma.Sounds get { return gain; } set { - gain = Math.Max(Math.Min(value, 1.0f), 0.0f); + gain = Math.Clamp(value, 0.0f, 1.0f); if (ALSourceIndex < 0) { return; } @@ -378,12 +404,12 @@ namespace Barotrauma.Sounds uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); if (!Al.IsSource(alSource)) return false; Al.GetSourcei(alSource, Al.SourceState, out state); - bool playing = state == Al.Playing; int alError = Al.GetError(); if (alError != Al.NoError) { throw new Exception("Failed to determine playing state from source: " + debugName + ", " + Al.GetErrorString(alError)); } + bool playing = state == Al.Playing; return playing; } } @@ -615,7 +641,7 @@ namespace Barotrauma.Sounds uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); int state; - Al.GetSourcei(Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.SourceState, out state); + Al.GetSourcei(alSource, Al.SourceState, out state); bool playing = state == Al.Playing; int alError = Al.GetError(); if (alError != Al.NoError) @@ -630,7 +656,7 @@ namespace Barotrauma.Sounds { throw new Exception("Failed to determine processed buffers from streamed source: " + debugName + ", " + Al.GetErrorString(alError)); } - + Al.SourceUnqueueBuffers(alSource, unqueuedBufferCount, unqueuedBuffers); alError = Al.GetError(); if (alError != Al.NoError) @@ -727,9 +753,20 @@ namespace Barotrauma.Sounds streamAmplitude = streamBufferAmplitudes[queueStartIndex]; Al.GetSourcei(alSource, Al.SourceState, out state); + alError = Al.GetError(); + if (alError != Al.NoError) + { + throw new Exception("Failed to retrieve stream source state: " + debugName + ", " + Al.GetErrorString(alError)); + } + if (state != Al.Playing) { Al.SourcePlay(alSource); + alError = Al.GetError(); + if (alError != Al.NoError) + { + throw new Exception("Failed to start stream playback: " + debugName + ", " + Al.GetErrorString(alError)); + } } } @@ -738,6 +775,10 @@ namespace Barotrauma.Sounds streamAmplitude = 0.0f; } } + catch (Exception e) + { + DebugConsole.ThrowError($"An exception was thrown when updating a sound stream ({debugName})", e); + } finally { Monitor.Exit(mutex); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs index bca402c77..e33862767 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs @@ -297,11 +297,11 @@ namespace Barotrauma.Sounds return newSound; } - public Sound LoadSound(XElement element, bool stream = false) + public Sound LoadSound(XElement element, bool stream = false, string overrideFilePath = null) { if (Disabled) { return null; } - string filePath = element.GetAttributeString("file", ""); + string filePath = overrideFilePath ?? element.GetAttributeString("file", ""); if (!File.Exists(filePath)) { throw new FileNotFoundException("Sound file \"" + filePath + "\" doesn't exist!"); @@ -621,7 +621,10 @@ namespace Barotrauma.Sounds } if (streamingThread == null || streamingThread.ThreadState.HasFlag(ThreadState.Stopped) || isStreamThreadDying) { - streamingThread?.Join(); + if (streamingThread != null && !streamingThread.Join(1000)) + { + DebugConsole.ThrowError("Sound stream thread join timed out!"); + } areStreamsPlaying = true; streamingThread = new Thread(UpdateStreaming) { @@ -639,10 +642,7 @@ namespace Barotrauma.Sounds bool killThread = false; while (!killThread) { - lock (threadDeathMutex) - { - areStreamsPlaying = false; - } + killThread = true; for (int i = 0; i < playingChannels.Length; i++) { lock (playingChannels[i]) @@ -654,10 +654,7 @@ namespace Barotrauma.Sounds { if (playingChannels[i][j].IsPlaying) { - lock (threadDeathMutex) - { - areStreamsPlaying = true; - } + killThread = false; playingChannels[i][j].UpdateStream(); } else @@ -678,7 +675,7 @@ namespace Barotrauma.Sounds } lock (threadDeathMutex) { - killThread = !areStreamsPlaying; + areStreamsPlaying = !killThread; } Thread.Sleep(10); //TODO: use a separate thread for network audio? } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs index 51ccff9aa..aa19c5ba6 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundPlayer.cs @@ -73,8 +73,8 @@ namespace Barotrauma private static float updateMusicTimer; //ambience - private static List waterAmbiences = new List(); - private static SoundChannel[] waterAmbienceChannels = new SoundChannel[2]; + private static Sound waterAmbienceIn, waterAmbienceOut, waterAmbienceMoving; + private static SoundChannel[] waterAmbienceChannels = new SoundChannel[3]; private static float ambientSoundTimer; private static Vector2 ambientSoundInterval = new Vector2(20.0f, 40.0f); //x = min, y = max @@ -123,7 +123,7 @@ namespace Barotrauma string filePathB = b.GetAttributeString("file", "").CleanUpPath(); float baseGainB = b.GetAttributeFloat("volume", 1.0f); float rangeB = b.GetAttributeFloat("range", 1000.0f); - return a.Name.ToString().ToLowerInvariant() == b.Name.ToString().ToLowerInvariant() && + return a.Name.ToString().Equals(b.Name.ToString(), StringComparison.OrdinalIgnoreCase) && filePathA == filePathB && MathUtils.NearlyEqual(baseGainA, baseGainB) && MathUtils.NearlyEqual(rangeA, rangeB); } @@ -148,10 +148,10 @@ namespace Barotrauma } soundElements.AddRange(mainElement.Elements()); } - + SoundCount = 1 + soundElements.Count(); - var startUpSoundElement = soundElements.Find(e => e.Name.ToString().ToLowerInvariant() == "startupsound"); + var startUpSoundElement = soundElements.Find(e => e.Name.ToString().Equals("startupsound", StringComparison.OrdinalIgnoreCase)); if (startUpSoundElement != null) { startUpSound = GameMain.SoundManager.LoadSound(startUpSoundElement, false); @@ -159,11 +159,13 @@ namespace Barotrauma } yield return CoroutineStatus.Running; - + List> miscSoundList = new List>(); - damageSounds = damageSounds ?? new List(); - musicClips = musicClips ?? new List(); - + damageSounds ??= new List(); + musicClips ??= new List(); + + bool firstWaterAmbienceLoaded = false; + foreach (XElement soundElement in soundElements) { yield return CoroutineStatus.Running; @@ -182,7 +184,7 @@ namespace Barotrauma musicClips.AddIfNotNull(newMusicClip); if (loadedSoundElements != null) { - if (newMusicClip.Type.ToLowerInvariant() == "menu") + if (newMusicClip.Type.Equals("menu", StringComparison.OrdinalIgnoreCase)) { targetMusic[0] = newMusicClip; } @@ -195,17 +197,57 @@ namespace Barotrauma FlowSounds.AddIfNotNull(GameMain.SoundManager.LoadSound(soundElement, false)); break; case "waterambience": - waterAmbiences.AddIfNotNull(GameMain.SoundManager.LoadSound(soundElement, false)); + //backwards compatibility (1st waterambience used to be played both inside and outside, 2nd when moving) + if (!firstWaterAmbienceLoaded) + { + waterAmbienceIn?.Dispose(); + waterAmbienceOut?.Dispose(); + if (File.Exists(soundElement.GetAttributeString("file", ""))) + { + waterAmbienceIn = GameMain.SoundManager.LoadSound(soundElement, false); + waterAmbienceOut = GameMain.SoundManager.LoadSound(soundElement, false); + } + else + { + waterAmbienceIn = GameMain.SoundManager.LoadSound(soundElement, false, "Content/Sounds/Water/WaterAmbienceIn.ogg"); + waterAmbienceOut = GameMain.SoundManager.LoadSound(soundElement, false, "Content/Sounds/Water/WaterAmbienceOut.ogg"); + } + firstWaterAmbienceLoaded = true; + } + else + { + waterAmbienceMoving?.Dispose(); + if (File.Exists(soundElement.GetAttributeString("file", ""))) + { + waterAmbienceMoving = GameMain.SoundManager.LoadSound(soundElement, false); + } + else + { + waterAmbienceMoving = GameMain.SoundManager.LoadSound(soundElement, false, "Content/Sounds/Water/WaterAmbienceMoving.ogg"); + } + } + break; + case "waterambiencein": + waterAmbienceIn?.Dispose(); + waterAmbienceIn = GameMain.SoundManager.LoadSound(soundElement, false); + break; + case "waterambienceout": + waterAmbienceOut?.Dispose(); + waterAmbienceOut = GameMain.SoundManager.LoadSound(soundElement, false); + break; + case "waterambiencemoving": + waterAmbienceMoving?.Dispose(); + waterAmbienceMoving = GameMain.SoundManager.LoadSound(soundElement, false); break; case "damagesound": Sound damageSound = GameMain.SoundManager.LoadSound(soundElement, false); if (damageSound == null) { continue; } - + string damageSoundType = soundElement.GetAttributeString("damagesoundtype", "None"); damageSounds.Add(new DamageSound( - damageSound, - soundElement.GetAttributeVector2("damagerange", Vector2.Zero), - damageSoundType, + damageSound, + soundElement.GetAttributeVector2("damagerange", Vector2.Zero), + damageSoundType, soundElement.GetAttributeString("requiredtag", ""))); break; @@ -221,12 +263,12 @@ namespace Barotrauma catch (FileNotFoundException e) { DebugConsole.ThrowError("Error while initializing SoundPlayer.", e); - } + } } musicClips.RemoveAll(mc => !soundElements.Any(e => SoundElementsEquivalent(mc.Element, e))); - for (int i=0;i mc.File == currentMusic[i].Filename)) { @@ -246,12 +288,6 @@ namespace Barotrauma }); FlowSounds.RemoveAll(s => s.Disposed); - waterAmbiences.ForEach(s => - { - if (!soundElements.Any(e => SoundElementsEquivalent(s.XElement, e))) { s.Dispose(); } - }); - waterAmbiences.RemoveAll(s => s.Disposed); - damageSounds.ForEach(s => { if (!soundElements.Any(e => SoundElementsEquivalent(s.sound.XElement, e))) { s.sound.Dispose(); } @@ -273,7 +309,7 @@ namespace Barotrauma fireVolumeLeft = new float[2]; fireVolumeRight = new float[2]; - miscSounds = miscSoundList.ToLookup(kvp => kvp.Key, kvp => kvp.Value); + miscSounds = miscSoundList.ToLookup(kvp => kvp.Key, kvp => kvp.Value); Initialized = true; @@ -282,7 +318,7 @@ namespace Barotrauma yield return CoroutineStatus.Success; } - + public static void Update(float deltaTime) { @@ -293,27 +329,27 @@ namespace Barotrauma if (startUpSound != null && !GameMain.SoundManager.IsPlaying(startUpSound)) { startUpSound.Dispose(); - startUpSound = null; + startUpSound = null; } //stop water sounds if no sub is loaded - if (Submarine.MainSub == null || Screen.Selected != GameMain.GameScreen) + if (Submarine.MainSub == null || Screen.Selected != GameMain.GameScreen) { for (int i = 0; i < waterAmbienceChannels.Length; i++) { - if (waterAmbienceChannels[i] == null) continue; + if (waterAmbienceChannels[i] == null) { continue; } waterAmbienceChannels[i].FadeOutAndDispose(); waterAmbienceChannels[i] = null; } for (int i = 0; i < FlowSounds.Count; i++) { - if (flowSoundChannels[i] == null) continue; + if (flowSoundChannels[i] == null) { continue; } flowSoundChannels[i].FadeOutAndDispose(); flowSoundChannels[i] = null; } for (int i = 0; i < fireSoundChannels.Length; i++) { - if (fireSoundChannels[i] == null) continue; + if (fireSoundChannels[i] == null) { continue; } fireSoundChannels[i].FadeOutAndDispose(); fireSoundChannels[i] = null; } @@ -333,17 +369,18 @@ namespace Barotrauma } } - UpdateWaterAmbience(ambienceVolume); + UpdateWaterAmbience(ambienceVolume, deltaTime); UpdateWaterFlowSounds(deltaTime); UpdateRandomAmbience(deltaTime); - UpdateFireSounds(deltaTime); + UpdateFireSounds(deltaTime); } - private static void UpdateWaterAmbience(float ambienceVolume) + private static void UpdateWaterAmbience(float ambienceVolume, float deltaTime) { //how fast the sub is moving, scaled to 0.0 -> 1.0 float movementSoundVolume = 0.0f; + float insideSubFactor = 0.0f; foreach (Submarine sub in Submarine.Loaded) { float movementFactor = (sub.Velocity == Vector2.Zero) ? 0.0f : sub.Velocity.Length() / 10.0f; @@ -352,7 +389,12 @@ namespace Barotrauma if (Character.Controlled == null || Character.Controlled.Submarine != sub) { float dist = Vector2.Distance(GameMain.GameScreen.Cam.WorldViewCenter, sub.WorldPosition); - movementFactor = movementFactor / Math.Max(dist / 1000.0f, 1.0f); + movementFactor /= Math.Max(dist / 1000.0f, 1.0f); + insideSubFactor = Math.Max(1.0f / Math.Max(dist / 1000.0f, 1.0f), insideSubFactor); + } + else + { + insideSubFactor = 1.0f; } movementSoundVolume = Math.Max(movementSoundVolume, movementFactor); @@ -365,28 +407,38 @@ namespace Barotrauma } } - if (waterAmbiences.Count > 1) + for (int i = 0; i < 3; i++) { - if (waterAmbienceChannels[0] == null || !waterAmbienceChannels[0].IsPlaying) + float volume = 0.0f; + Sound sound = null; + switch (i) { - waterAmbienceChannels[0] = waterAmbiences[0].Play(ambienceVolume * (1.0f - movementSoundVolume),"waterambience"); - //waterAmbiences[0].Loop(waterAmbienceIndexes[0], ambienceVolume * (1.0f - movementSoundVolume)); - waterAmbienceChannels[0].Looping = true; - } - else - { - waterAmbienceChannels[0].Gain = ambienceVolume * (1.0f - movementSoundVolume); + case 0: + volume = ambienceVolume * (1.0f - movementSoundVolume) * insideSubFactor; + sound = waterAmbienceIn; + break; + case 1: + volume = ambienceVolume * movementSoundVolume * insideSubFactor; + sound = waterAmbienceMoving; + break; + case 2: + volume = 1.0f - insideSubFactor; + sound = waterAmbienceOut; + break; } - if (waterAmbienceChannels[1] == null || !waterAmbienceChannels[1].IsPlaying) + if ((waterAmbienceChannels[i] == null || !waterAmbienceChannels[i].IsPlaying) && volume > 0.01f) { - waterAmbienceChannels[1] = waterAmbiences[1].Play(ambienceVolume * movementSoundVolume, "waterambience"); - //waterAmbienceIndexes[1] = waterAmbiences[1].Loop(waterAmbienceIndexes[1], ambienceVolume * movementSoundVolume); - waterAmbienceChannels[1].Looping = true; + waterAmbienceChannels[i] = sound.Play(volume, "waterambience"); + waterAmbienceChannels[i].Looping = true; } - else + else if (waterAmbienceChannels[i] != null) { - waterAmbienceChannels[1].Gain = ambienceVolume * movementSoundVolume; + waterAmbienceChannels[i].Gain += deltaTime * Math.Sign(volume - waterAmbienceChannels[i].Gain); + if (waterAmbienceChannels[i].Gain < 0.01f) + { + waterAmbienceChannels[i].FadeOutAndDispose(); + } } } } @@ -419,11 +471,11 @@ namespace Barotrauma //flow at the left side if (diff.X < 0) { - targetFlowLeft[flowSoundIndex] = 1.0f - distFallOff; + targetFlowLeft[flowSoundIndex] += 1.0f - distFallOff; } else { - targetFlowRight[flowSoundIndex] = 1.0f - distFallOff; + targetFlowRight[flowSoundIndex] += 1.0f - distFallOff; } } } @@ -742,22 +794,30 @@ namespace Barotrauma Screen.Selected == GameMain.LevelEditorScreen || Screen.Selected == GameMain.ParticleEditorScreen || Screen.Selected == GameMain.SpriteEditorScreen || - Screen.Selected == GameMain.SubEditorScreen) + Screen.Selected == GameMain.SubEditorScreen || + (Screen.Selected == GameMain.GameScreen && GameMain.GameSession?.GameMode is SubTestMode)) { return "editor"; } if (Screen.Selected != GameMain.GameScreen) { return "menu"; } - if (Character.Controlled != null && - Level.Loaded != null && Level.Loaded.Ruins != null && - Level.Loaded.Ruins.Any(r => r.Area.Contains(Character.Controlled.WorldPosition))) + + if (Character.Controlled != null) { - return "ruins"; + if (Level.Loaded != null && Level.Loaded.Ruins != null && + Level.Loaded.Ruins.Any(r => r.Area.Contains(Character.Controlled.WorldPosition))) + { + return "ruins"; + } + + if (Character.Controlled.Submarine?.Info?.IsWreck ?? false) + { + return "wreck"; + } } Submarine targetSubmarine = Character.Controlled?.Submarine; - if ((targetSubmarine != null && targetSubmarine.AtDamageDepth) || (GameMain.GameScreen != null && Screen.Selected == GameMain.GameScreen && GameMain.GameScreen.Cam.Position.Y < SubmarineBody.DamageDepth)) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs index bd137b800..734925a07 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/VoipSound.cs @@ -44,13 +44,15 @@ namespace Barotrauma.Sounds new BandpassFilter(VoipConfig.FREQUENCY, 2000) }; + private float gain; public float Gain { - get { return soundChannel == null ? 0.0f : soundChannel.Gain; } + get { return soundChannel == null ? 0.0f : gain; } set { if (soundChannel == null) { return; } - soundChannel.Gain = value; + gain = value; + soundChannel.Gain = value * GameMain.Config.VoiceChatVolume; } } @@ -59,8 +61,10 @@ namespace Barotrauma.Sounds get { return soundChannel?.CurrentAmplitude ?? 0.0f; } } - public VoipSound(SoundManager owner, VoipQueue q) : base(owner, "voip", true, true) + public VoipSound(string name, SoundManager owner, VoipQueue q) : base(owner, "voip", true, true) { + Filename = $"VoIP ({name})"; + VoipConfig.SetupEncoding(); ALFormat = Al.FormatMono16; @@ -73,6 +77,7 @@ namespace Barotrauma.Sounds SoundChannel chn = new SoundChannel(this, 1.0f, null, 0.4f, 1.0f, "voip", false); soundChannel = chn; + Gain = 1.0f; } public override float GetAmplitudeAtPlaybackPos(int playbackPos) @@ -92,26 +97,29 @@ namespace Barotrauma.Sounds } public void ApplyFilters(short[] buffer, int readSamples) - { - if (UseMuffleFilter) - { - ApplyFilters(radioFilters, buffer, readSamples); - } - - if (UseRadioFilter) - { - ApplyFilters(radioFilters, buffer, readSamples); - } - } - - private void ApplyFilters(IEnumerable filters, short[] buffer, int readSamples) { for (int i = 0; i < readSamples; i++) { float fVal = ShortToFloat(buffer[i]); - foreach (var filter in filters) + + if (gain * GameMain.Config.VoiceChatVolume > 1.0f) //TODO: take distance into account? { - fVal = filter.Process(fVal); + fVal = Math.Clamp(fVal * gain * GameMain.Config.VoiceChatVolume, -1.0f, 1.0f); + } + + if (UseMuffleFilter) + { + foreach (var filter in muffleFilters) + { + fVal = filter.Process(fVal); + } + } + if (UseRadioFilter) + { + foreach (var filter in radioFilters) + { + fVal = filter.Process(fVal); + } } buffer[i] = FloatToShort(fVal); } @@ -140,13 +148,21 @@ namespace Barotrauma.Sounds public override int FillStreamBuffer(int samplePos, short[] buffer) { queue.RetrieveBuffer(bufferID, out int compressedSize, out byte[] compressedBuffer); - if (compressedSize > 0) + try { - VoipConfig.Decoder.Decode(compressedBuffer, 0, compressedSize, buffer, 0, VoipConfig.BUFFER_SIZE); - bufferID++; - return VoipConfig.BUFFER_SIZE * 2; + if (compressedSize > 0) + { + VoipConfig.Decoder.Decode(compressedBuffer, 0, compressedSize, buffer, 0, VoipConfig.BUFFER_SIZE); + bufferID++; + return VoipConfig.BUFFER_SIZE * 2; + } + if (bufferID < queue.LatestBufferID - (VoipQueue.BUFFER_COUNT - 1)) bufferID = queue.LatestBufferID - (VoipQueue.BUFFER_COUNT - 1); + } + catch (Exception e) + { + DebugConsole.ThrowError($"Failed to decode Opus buffer (buffer size {compressedBuffer.Length}, packet size {compressedSize})", e); + bufferID = queue.LatestBufferID - (VoipQueue.BUFFER_COUNT - 1); } - if (bufferID < queue.LatestBufferID - (VoipQueue.BUFFER_COUNT - 1)) bufferID = queue.LatestBufferID - (VoipQueue.BUFFER_COUNT - 1); return 0; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs index 10b117dd4..64fc96a80 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DecorativeSprite.cs @@ -2,12 +2,18 @@ using System; using System.Collections.Generic; using System.Xml.Linq; -using SpriteParams = Barotrauma.RagdollParams.SpriteParams; namespace Barotrauma { class DecorativeSprite : ISerializableEntity { + public class State + { + public float RotationState; + public float OffsetState; + public bool IsActive = true; + } + public string Name => $"Decorative Sprite"; public Dictionary SerializableProperties { get; set; } @@ -57,6 +63,14 @@ namespace Barotrauma } } + private float scale; + [Serialize(1.0f, true), Editable] + public float Scale + { + get { return scale; } + private set { scale = MathHelper.Clamp(value, 0.0f, 10.0f); } + } + [Serialize(AnimationType.None, false), Editable] public AnimationType RotationAnim { get; private set; } @@ -66,6 +80,9 @@ namespace Barotrauma [Serialize(0, false, description: "If > 0, only one sprite of the same group is used (chosen randomly)"), Editable(ReadOnly = true)] public int RandomGroupID { get; private set; } + [Serialize("1.0,1.0,1.0,1.0", true), Editable()] + public Color Color { get; set; } + /// /// The sprite is only drawn if these conditions are fulfilled /// @@ -113,10 +130,10 @@ namespace Barotrauma switch (OffsetAnim) { case AnimationType.Sine: - offsetState = offsetState % (MathHelper.TwoPi / OffsetAnimSpeed); + offsetState %= (MathHelper.TwoPi / OffsetAnimSpeed); return Offset * (float)Math.Sin(offsetState * OffsetAnimSpeed); case AnimationType.Noise: - offsetState = offsetState % (1.0f / (OffsetAnimSpeed * 0.1f)); + offsetState %= (1.0f / (OffsetAnimSpeed * 0.1f)); float t = offsetState * 0.1f * OffsetAnimSpeed; return new Vector2( @@ -146,6 +163,51 @@ namespace Barotrauma } } + public static void UpdateSpriteStates(Dictionary> spriteGroups, Dictionary animStates, + int entityID, float deltaTime, Func checkConditional) + { + foreach (int spriteGroup in spriteGroups.Keys) + { + for (int i = 0; i < spriteGroups[spriteGroup].Count; i++) + { + var decorativeSprite = spriteGroups[spriteGroup][i]; + if (decorativeSprite == null) { continue; } + if (spriteGroup > 0) + { + int activeSpriteIndex = entityID % spriteGroups[spriteGroup].Count; + if (i != activeSpriteIndex) + { + animStates[decorativeSprite].IsActive = false; + continue; + } + } + + //check if the sprite is active (whether it should be drawn or not) + var spriteState = animStates[decorativeSprite]; + spriteState.IsActive = true; + foreach (PropertyConditional conditional in decorativeSprite.IsActiveConditionals) + { + if (!checkConditional(conditional)) + { + spriteState.IsActive = false; + break; + } + } + if (!spriteState.IsActive) { continue; } + + //check if the sprite should be animated + bool animate = true; + foreach (PropertyConditional conditional in decorativeSprite.AnimationConditionals) + { + if (!checkConditional(conditional)) { animate = false; break; } + } + if (!animate) { continue; } + spriteState.OffsetState += deltaTime; + spriteState.RotationState += deltaTime; + } + } + } + public void Remove() { Sprite?.Remove(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/SpriteDeformation.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/SpriteDeformation.cs index 4df76b354..d88669f5a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/SpriteDeformation.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/DeformAnimations/SpriteDeformation.cs @@ -49,6 +49,9 @@ namespace Barotrauma.SpriteDeformations [Serialize(false, true), Editable] public bool StopWhenHostIsDead { get; set; } + [Serialize(false, true), Editable] + public bool OnlyInWater { get; set; } + /// /// Only used if UseMovementSine is enabled. Multiplier for Pi. /// diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs index c7beda481..334662613 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Sprite/Sprite.cs @@ -21,6 +21,11 @@ namespace Barotrauma } } + public bool Loaded + { + get { return texture != null && !cannotBeLoaded; } + } + public Sprite(Texture2D texture, Rectangle? sourceRectangle, Vector2? newOffset, float newRotation = 0.0f) { this.texture = texture; @@ -42,7 +47,12 @@ namespace Barotrauma partial void LoadTexture(ref Vector4 sourceVector, ref bool shouldReturn) { - texture = LoadTexture(this.FilePath); + texture = LoadTexture(this.FilePath, out Sprite reusedSprite); + if (reusedSprite != null) + { + FilePath = string.Intern(reusedSprite.FilePath); + FullPath = string.Intern(reusedSprite.FullPath); + } if (texture == null) { @@ -56,7 +66,7 @@ namespace Barotrauma public void EnsureLazyLoaded() { - if (!lazyLoad || texture != null || cannotBeLoaded) { return; } + if (!LazyLoad || texture != null || cannotBeLoaded) { return; } Vector4 sourceVector = Vector4.Zero; bool temp2 = false; @@ -69,11 +79,6 @@ namespace Barotrauma size.Y *= sourceRect.Height; RelativeOrigin = SourceElement.GetAttributeVector2("origin", new Vector2(0.5f, 0.5f)); } - foreach (Sprite s in LoadedSprites) - { - if (s == this) { continue; } - if (s.FullPath == FullPath && s.texture != null) { s.texture = texture; } - } if (texture == null) { cannotBeLoaded = true; @@ -99,6 +104,12 @@ namespace Barotrauma public static Texture2D LoadTexture(string file) { + return LoadTexture(file, out _); + } + + public static Texture2D LoadTexture(string file, out Sprite reusedSprite) + { + reusedSprite = null; if (string.IsNullOrWhiteSpace(file)) { Texture2D t = null; @@ -111,7 +122,11 @@ namespace Barotrauma file = Path.GetFullPath(file); foreach (Sprite s in LoadedSprites) { - if (s.FullPath == file && s.texture != null && !s.texture.IsDisposed) { return s.texture; } + if (s.FullPath == file && s.texture != null && !s.texture.IsDisposed) + { + reusedSprite = s; + return s.texture; + } } if (File.Exists(file)) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionPrefab.cs index 9a900cf4c..42d8d7f84 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Traitors/TraitorMissionPrefab.cs @@ -35,7 +35,7 @@ namespace Barotrauma Identifier = element.GetAttributeString("identifier", ""); foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() == "icon") + if (subElement.Name.ToString().Equals("icon", StringComparison.OrdinalIgnoreCase)) { Icon = new Sprite(subElement); IconColor = subElement.GetAttributeColor("color", Color.White); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/Quad.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/Quad.cs new file mode 100644 index 000000000..b373c9e7e --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/Quad.cs @@ -0,0 +1,67 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Barotrauma +{ + static class Quad + { + private static VertexBuffer vertexBuffer = null; + private static IndexBuffer indexBuffer = null; + private static BasicEffect basicEffect = null; + private static GraphicsDevice graphicsDevice = null; + + public static void Init(GraphicsDevice graphics) + { + if (graphicsDevice != null) { return; } + + graphicsDevice = graphics; + + vertexBuffer = new VertexBuffer(graphics, VertexPositionTexture.VertexDeclaration, 4, BufferUsage.WriteOnly); + indexBuffer = new IndexBuffer(graphics, IndexElementSize.SixteenBits, 4, BufferUsage.WriteOnly); + + InitVertexData(); + indexBuffer.SetData(new ushort[] { 0, 1, 2, 3 }); + + basicEffect = new BasicEffect(graphics) { TextureEnabled = true }; + + GameMain.Instance.OnResolutionChanged += () => + { + InitVertexData(); + }; + } + + private static void InitVertexData() + { + Vector2 halfPixelOffset = Vector2.Zero; +#if LINUX || OSX + halfPixelOffset = new Vector2(0.5f / GameMain.GraphicsWidth, 0.5f / GameMain.GraphicsHeight); +#endif + + VertexPositionTexture[] vertices = + { + new VertexPositionTexture(new Vector3(-1f, -1f, 1f), new Vector2(0f, 1f) + halfPixelOffset), + new VertexPositionTexture(new Vector3(-1f, 1f, 1f), new Vector2(0f, 0f) + halfPixelOffset), + new VertexPositionTexture(new Vector3(1f, -1f, 1f), new Vector2(1f, 1f) + halfPixelOffset), + new VertexPositionTexture(new Vector3(1f, 1f, 1f), new Vector2(1f, 0f) + halfPixelOffset) + }; + + vertexBuffer.SetData(vertices); + } + + public static void UseBasicEffect(Texture2D texture) + { + basicEffect.Texture = texture; + basicEffect.CurrentTechnique.Passes[0].Apply(); + } + + public static void Render() + { + graphicsDevice.SetVertexBuffer(vertexBuffer); + graphicsDevice.Indices = indexBuffer; + graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleStrip, 0, 0, 2); + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs index ad2518771..c04d65ed9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Utils/ToolBox.cs @@ -240,13 +240,13 @@ namespace Barotrauma name = null; endpoint = null; lobbyId = 0; if (args == null || args.Length < 2) { return; } - if (args[0].Equals("-connect", StringComparison.InvariantCultureIgnoreCase)) + if (args[0].Equals("-connect", StringComparison.OrdinalIgnoreCase)) { if (args.Length < 3) { return; } name = args[1]; endpoint = args[2]; } - else if (args[0].Equals("+connect_lobby", StringComparison.InvariantCultureIgnoreCase)) + else if (args[0].Equals("+connect_lobby", StringComparison.OrdinalIgnoreCase)) { UInt64.TryParse(args[1], out lobbyId); } diff --git a/Barotrauma/BarotraumaClient/Content/Effects/deformshader.xnb b/Barotrauma/BarotraumaClient/Content/Effects/deformshader.xnb index 35b0ae81b..f01c765ee 100644 Binary files a/Barotrauma/BarotraumaClient/Content/Effects/deformshader.xnb and b/Barotrauma/BarotraumaClient/Content/Effects/deformshader.xnb differ diff --git a/Barotrauma/BarotraumaClient/Content/Effects/deformshader_opengl.xnb b/Barotrauma/BarotraumaClient/Content/Effects/deformshader_opengl.xnb index 15bed2a78..abe594f01 100644 Binary files a/Barotrauma/BarotraumaClient/Content/Effects/deformshader_opengl.xnb and b/Barotrauma/BarotraumaClient/Content/Effects/deformshader_opengl.xnb differ diff --git a/Barotrauma/BarotraumaClient/Content/Effects/losshader.xnb b/Barotrauma/BarotraumaClient/Content/Effects/losshader.xnb index 3d0a76ecb..b17c38ef3 100644 Binary files a/Barotrauma/BarotraumaClient/Content/Effects/losshader.xnb and b/Barotrauma/BarotraumaClient/Content/Effects/losshader.xnb differ diff --git a/Barotrauma/BarotraumaClient/Content/Effects/losshader_opengl.xnb b/Barotrauma/BarotraumaClient/Content/Effects/losshader_opengl.xnb index 96032c202..bd4516465 100644 Binary files a/Barotrauma/BarotraumaClient/Content/Effects/losshader_opengl.xnb and b/Barotrauma/BarotraumaClient/Content/Effects/losshader_opengl.xnb differ diff --git a/Barotrauma/BarotraumaClient/Content/Effects/postprocess.xnb b/Barotrauma/BarotraumaClient/Content/Effects/postprocess.xnb index 91cf8a795..f55a97a59 100644 Binary files a/Barotrauma/BarotraumaClient/Content/Effects/postprocess.xnb and b/Barotrauma/BarotraumaClient/Content/Effects/postprocess.xnb differ diff --git a/Barotrauma/BarotraumaClient/Content/Effects/postprocess_opengl.xnb b/Barotrauma/BarotraumaClient/Content/Effects/postprocess_opengl.xnb index 8c8308fcc..acc1f0930 100644 Binary files a/Barotrauma/BarotraumaClient/Content/Effects/postprocess_opengl.xnb and b/Barotrauma/BarotraumaClient/Content/Effects/postprocess_opengl.xnb differ diff --git a/Barotrauma/BarotraumaClient/Content/Effects/solidcolor.xnb b/Barotrauma/BarotraumaClient/Content/Effects/solidcolor.xnb index 8d2819b44..33684d393 100644 Binary files a/Barotrauma/BarotraumaClient/Content/Effects/solidcolor.xnb and b/Barotrauma/BarotraumaClient/Content/Effects/solidcolor.xnb differ diff --git a/Barotrauma/BarotraumaClient/Content/Effects/solidcolor_opengl.xnb b/Barotrauma/BarotraumaClient/Content/Effects/solidcolor_opengl.xnb index efbff6947..d27af4781 100644 Binary files a/Barotrauma/BarotraumaClient/Content/Effects/solidcolor_opengl.xnb and b/Barotrauma/BarotraumaClient/Content/Effects/solidcolor_opengl.xnb differ diff --git a/Barotrauma/BarotraumaClient/Content/Effects/watershader.xnb b/Barotrauma/BarotraumaClient/Content/Effects/watershader.xnb index 680d837b5..e5bd61ae4 100644 Binary files a/Barotrauma/BarotraumaClient/Content/Effects/watershader.xnb and b/Barotrauma/BarotraumaClient/Content/Effects/watershader.xnb differ diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 0f4542084..83a5aee27 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.9.8.0 + 0.9.9.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 31e5057fe..7e1e8ffa1 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.9.8.0 + 0.9.9.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/Shaders/Content_opengl.mgcb b/Barotrauma/BarotraumaClient/Shaders/Content_opengl.mgcb index 2d201daec..e18949d5d 100644 --- a/Barotrauma/BarotraumaClient/Shaders/Content_opengl.mgcb +++ b/Barotrauma/BarotraumaClient/Shaders/Content_opengl.mgcb @@ -19,11 +19,6 @@ /processorParam:DebugMode=Auto /build:blurshader_opengl.fx -#begin Content_opengl.mgcb -/importer: -/processor: -/build:Content_opengl.mgcb - #begin damageshader_opengl.fx /importer:EffectImporter /processor:EffectProcessor diff --git a/Barotrauma/BarotraumaClient/Shaders/blurshader.fx b/Barotrauma/BarotraumaClient/Shaders/blurshader.fx index 45c4cd015..54a5f1e81 100644 --- a/Barotrauma/BarotraumaClient/Shaders/blurshader.fx +++ b/Barotrauma/BarotraumaClient/Shaders/blurshader.fx @@ -10,7 +10,7 @@ float2 SampleOffsets[SAMPLE_COUNT]; float SampleWeights[SAMPLE_COUNT]; -float4 PixelShaderF(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 PixelShaderF(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { float4 c = 0; diff --git a/Barotrauma/BarotraumaClient/Shaders/blurshader_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/blurshader_opengl.fx index 14aff9d67..4955b7d58 100644 --- a/Barotrauma/BarotraumaClient/Shaders/blurshader_opengl.fx +++ b/Barotrauma/BarotraumaClient/Shaders/blurshader_opengl.fx @@ -10,7 +10,7 @@ float2 SampleOffsets[SAMPLE_COUNT]; float SampleWeights[SAMPLE_COUNT]; -float4 PixelShaderF(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 PixelShaderF(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { float4 c = 0; diff --git a/Barotrauma/BarotraumaClient/Shaders/damageshader.fx b/Barotrauma/BarotraumaClient/Shaders/damageshader.fx index 8444fd9c6..6f0781293 100644 --- a/Barotrauma/BarotraumaClient/Shaders/damageshader.fx +++ b/Barotrauma/BarotraumaClient/Shaders/damageshader.fx @@ -13,7 +13,7 @@ float aMultiplier; float cCutoff; float cMultiplier; -float4 main(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { float4 c = xTexture.Sample(TextureSampler, texCoord) * inColor; diff --git a/Barotrauma/BarotraumaClient/Shaders/damageshader_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/damageshader_opengl.fx index c50fe7fb9..3a4242a3a 100644 --- a/Barotrauma/BarotraumaClient/Shaders/damageshader_opengl.fx +++ b/Barotrauma/BarotraumaClient/Shaders/damageshader_opengl.fx @@ -13,7 +13,7 @@ float aMultiplier; float cCutoff; float cMultiplier; -float4 main(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { float4 c = tex2D(TextureSampler, texCoord) * inColor; diff --git a/Barotrauma/BarotraumaClient/Shaders/deformshader.fx b/Barotrauma/BarotraumaClient/Shaders/deformshader.fx index dcf54addd..f7db6f974 100644 --- a/Barotrauma/BarotraumaClient/Shaders/deformshader.fx +++ b/Barotrauma/BarotraumaClient/Shaders/deformshader.fx @@ -18,14 +18,14 @@ float4 solidColor; struct VertexShaderInput { - float4 Position : SV_POSITION; + float4 Position : POSITION0; float4 Color : COLOR0; float2 TexCoords: TEXCOORD0; }; struct VertexShaderOutput { - float4 Position : SV_POSITION; + float4 Position : POSITION0; float4 Color : COLOR0; float2 TexCoords: TEXCOORD0; }; diff --git a/Barotrauma/BarotraumaClient/Shaders/deformshader_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/deformshader_opengl.fx index c77904ffe..909d1f89e 100644 --- a/Barotrauma/BarotraumaClient/Shaders/deformshader_opengl.fx +++ b/Barotrauma/BarotraumaClient/Shaders/deformshader_opengl.fx @@ -18,14 +18,14 @@ float4 solidColor; struct VertexShaderInput { - float4 Position : SV_POSITION; + float4 Position : POSITION0; float4 Color : COLOR0; float2 TexCoords: TEXCOORD0; }; struct VertexShaderOutput { - float4 Position : SV_POSITION; + float4 Position : POSITION0; float4 Color : COLOR0; float2 TexCoords: TEXCOORD0; }; diff --git a/Barotrauma/BarotraumaClient/Shaders/gradientshader.fx b/Barotrauma/BarotraumaClient/Shaders/gradientshader.fx index 26e364323..54dad1b92 100644 --- a/Barotrauma/BarotraumaClient/Shaders/gradientshader.fx +++ b/Barotrauma/BarotraumaClient/Shaders/gradientshader.fx @@ -5,7 +5,7 @@ float4 color2; float midPoint; float fadeDist; -float4 PixelShaderF(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 PixelShaderF(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { float4 t = tex2D(TextureSampler, texCoord); diff --git a/Barotrauma/BarotraumaClient/Shaders/gradientshader_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/gradientshader_opengl.fx index ab209fa73..cbb6bf945 100644 --- a/Barotrauma/BarotraumaClient/Shaders/gradientshader_opengl.fx +++ b/Barotrauma/BarotraumaClient/Shaders/gradientshader_opengl.fx @@ -5,7 +5,7 @@ float4 color2; float midPoint; float fadeDist; -float4 PixelShaderF(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 PixelShaderF(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { float4 t = tex2D(TextureSampler, texCoord); diff --git a/Barotrauma/BarotraumaClient/Shaders/losshader.fx b/Barotrauma/BarotraumaClient/Shaders/losshader.fx index 357e2bad9..d40f91c49 100644 --- a/Barotrauma/BarotraumaClient/Shaders/losshader.fx +++ b/Barotrauma/BarotraumaClient/Shaders/losshader.fx @@ -1,3 +1,24 @@ +struct VertexShaderInput +{ + float4 Position : POSITION0; + float2 TexCoords: TEXCOORD0; +}; + +struct VertexShaderOutput +{ + float4 Position : POSITION0; + float2 TexCoords: TEXCOORD0; +}; + +VertexShaderOutput mainVS(in VertexShaderInput input) +{ + VertexShaderOutput output = (VertexShaderOutput)0; + + output.Position = input.Position; + output.TexCoords = input.TexCoords; + + return output; +} Texture2D xTexture; sampler TextureSampler : register (s0) = sampler_state { Texture = ; }; @@ -5,17 +26,19 @@ sampler TextureSampler : register (s0) = sampler_state { Texture = ; } Texture2D xLosTexture; sampler LosSampler = sampler_state { Texture = ; }; -float4 main(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 xColor; + +float4 mainPS(VertexShaderOutput input) : COLOR0 { - float4 sampleColor = xTexture.Sample(TextureSampler, texCoord); - float4 losColor = xLosTexture.Sample(LosSampler, texCoord); + float4 sampleColor = xTexture.Sample(TextureSampler, input.TexCoords); + float4 losColor = xLosTexture.Sample(LosSampler, input.TexCoords); float obscureAmount = 1.0f - losColor.r; float4 outColor = float4( - sampleColor.r * color.r, - sampleColor.g * color.g, - sampleColor.b * color.b, + sampleColor.r * xColor.r, + sampleColor.g * xColor.g, + sampleColor.b * xColor.b, obscureAmount); return outColor; @@ -25,6 +48,7 @@ technique LosShader { pass Pass1 { - PixelShader = compile ps_4_0_level_9_1 main(); + VertexShader = compile vs_4_0_level_9_1 mainVS(); + PixelShader = compile ps_4_0_level_9_1 mainPS(); } } diff --git a/Barotrauma/BarotraumaClient/Shaders/losshader_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/losshader_opengl.fx index 98805c7ca..23a48e4d2 100644 --- a/Barotrauma/BarotraumaClient/Shaders/losshader_opengl.fx +++ b/Barotrauma/BarotraumaClient/Shaders/losshader_opengl.fx @@ -1,3 +1,24 @@ +struct VertexShaderInput +{ + float4 Position : POSITION0; + float2 TexCoords: TEXCOORD0; +}; + +struct VertexShaderOutput +{ + float4 Position : POSITION0; + float2 TexCoords: TEXCOORD0; +}; + +VertexShaderOutput mainVS(in VertexShaderInput input) +{ + VertexShaderOutput output = (VertexShaderOutput)0; + + output.Position = input.Position; + output.TexCoords = input.TexCoords; + + return output; +} Texture xTexture; sampler TextureSampler : register (s0) = sampler_state { Texture = ; }; @@ -5,17 +26,19 @@ sampler TextureSampler : register (s0) = sampler_state { Texture = ; } Texture xLosTexture; sampler LosSampler = sampler_state { Texture = ; }; -float4 main(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 xColor; + +float4 mainPS(VertexShaderOutput input) : COLOR0 { - float4 sampleColor = tex2D(TextureSampler, texCoord); - float4 losColor = tex2D(LosSampler, texCoord); + float4 sampleColor = tex2D(TextureSampler, input.TexCoords); + float4 losColor = tex2D(LosSampler, input.TexCoords); float obscureAmount = 1.0f - losColor.r; float4 outColor = float4( - sampleColor.r * color.r, - sampleColor.g * color.g, - sampleColor.b * color.b, + sampleColor.r * xColor.r, + sampleColor.g * xColor.g, + sampleColor.b * xColor.b, obscureAmount); return outColor; @@ -25,6 +48,7 @@ technique LosShader { pass Pass1 { - PixelShader = compile ps_2_0 main(); + VertexShader = compile vs_2_0 mainVS(); + PixelShader = compile ps_2_0 mainPS(); } } diff --git a/Barotrauma/BarotraumaClient/Shaders/postprocess.fx b/Barotrauma/BarotraumaClient/Shaders/postprocess.fx index 214ba721d..e618bf0bd 100644 --- a/Barotrauma/BarotraumaClient/Shaders/postprocess.fx +++ b/Barotrauma/BarotraumaClient/Shaders/postprocess.fx @@ -1,3 +1,27 @@ +struct VertexShaderInput +{ + float4 Position : POSITION0; + float2 TexCoords: TEXCOORD0; +}; + +struct VertexShaderOutput +{ + float4 Position : POSITION0; + float2 TexCoords: TEXCOORD0; +}; + +float4x4 MatrixTransform; + +VertexShaderOutput mainVS(in VertexShaderInput input) +{ + VertexShaderOutput output = (VertexShaderOutput)0; + + output.Position = mul(input.Position, MatrixTransform); + output.TexCoords = input.TexCoords; + + return output; +} + Texture2D xTexture; sampler TextureSampler = sampler_state { Texture = ; }; @@ -32,84 +56,84 @@ float2 radialDistortion(float2 coord, float distortion) /*float4 sampleWithChromaticAberration(float2 samplePos) { return float4( - tex2D(TextureSampler, radialDistortion(samplePos, chromaticAberrationStrength.r)).r, - tex2D(TextureSampler, radialDistortion(samplePos, chromaticAberrationStrength.g)).g, - tex2D(TextureSampler, radialDistortion(samplePos, chromaticAberrationStrength.b)).b, + xTexture.Sample(TextureSampler, radialDistortion(samplePos, chromaticAberrationStrength.r)).r, + xTexture.Sample(TextureSampler, radialDistortion(samplePos, chromaticAberrationStrength.g)).g, + xTexture.Sample(TextureSampler, radialDistortion(samplePos, chromaticAberrationStrength.b)).b, 1); }*/ float3 sampleChannelsSeparately(float2 samplePosR, float2 samplePosG, float2 samplePosB) { return float3( - tex2D(TextureSampler, samplePosR).r, - tex2D(TextureSampler, samplePosG).g, - tex2D(TextureSampler, samplePosB).b); + xTexture.Sample(TextureSampler, samplePosR).r, + xTexture.Sample(TextureSampler, samplePosG).g, + xTexture.Sample(TextureSampler, samplePosB).b); } float3 sampleWithChromaticAberration(float2 samplePos) { return float3( - tex2D(TextureSampler, radialDistortion(samplePos, chromaticAberrationStrength.r)).r, - tex2D(TextureSampler, radialDistortion(samplePos, chromaticAberrationStrength.g)).g, - tex2D(TextureSampler, radialDistortion(samplePos, chromaticAberrationStrength.b)).b); + xTexture.Sample(TextureSampler, radialDistortion(samplePos, chromaticAberrationStrength.r)).r, + xTexture.Sample(TextureSampler, radialDistortion(samplePos, chromaticAberrationStrength.g)).g, + xTexture.Sample(TextureSampler, radialDistortion(samplePos, chromaticAberrationStrength.b)).b); } -float4 blur(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 blur(VertexShaderOutput input) : COLOR0 { float4 sample; - sample = tex2D(TextureSampler, float2(texCoord.x + blurDistance, texCoord.y + blurDistance)); - sample += tex2D(TextureSampler, float2(texCoord.x - blurDistance, texCoord.y - blurDistance)); - sample += tex2D(TextureSampler, float2(texCoord.x + blurDistance, texCoord.y - blurDistance)); - sample += tex2D(TextureSampler, float2(texCoord.x - blurDistance, texCoord.y + blurDistance)); + sample = xTexture.Sample(TextureSampler, float2(input.TexCoords.x + blurDistance, input.TexCoords.y + blurDistance)); + sample += xTexture.Sample(TextureSampler, float2(input.TexCoords.x - blurDistance, input.TexCoords.y - blurDistance)); + sample += xTexture.Sample(TextureSampler, float2(input.TexCoords.x + blurDistance, input.TexCoords.y - blurDistance)); + sample += xTexture.Sample(TextureSampler, float2(input.TexCoords.x - blurDistance, input.TexCoords.y + blurDistance)); sample = sample * 0.25f; return sample; } -float4 distort(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 distort(VertexShaderOutput input) : COLOR0 { - float4 bumpColor = tex2D(DistortSampler, texCoord + distortUvOffset); - bumpColor = (bumpColor + tex2D(DistortSampler, texCoord - distortUvOffset * 2.0f)) * 0.5f; + float4 bumpColor = xDistortTexture.Sample(DistortSampler, input.TexCoords + distortUvOffset); + bumpColor = (bumpColor + xDistortTexture.Sample(DistortSampler, input.TexCoords - distortUvOffset * 2.0f)) * 0.5f; - float2 samplePos = texCoord; + float2 samplePos = input.TexCoords; samplePos.x += (bumpColor.r - 0.5f) * distortScale.x; samplePos.y += (bumpColor.g - 0.5f) * distortScale.y; - return tex2D(TextureSampler, samplePos); + return xTexture.Sample(TextureSampler, samplePos); } -float4 blurDistort(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 blurDistort(VertexShaderOutput input) : COLOR0 { - float4 bumpColor = tex2D(DistortSampler, texCoord + distortUvOffset); - bumpColor = (bumpColor + tex2D(DistortSampler, texCoord - distortUvOffset * 2.0f)) * 0.5f; + float4 bumpColor = xDistortTexture.Sample(DistortSampler, input.TexCoords + distortUvOffset); + bumpColor = (bumpColor + xDistortTexture.Sample(DistortSampler, input.TexCoords - distortUvOffset * 2.0f)) * 0.5f; - float2 samplePos = texCoord; + float2 samplePos = input.TexCoords; samplePos.x += (bumpColor.r - 0.5f) * distortScale.x; samplePos.y += (bumpColor.g - 0.5f) * distortScale.y; float4 sample; - sample = tex2D(TextureSampler, float2(samplePos.x + blurDistance, samplePos.y + blurDistance)); - sample += tex2D(TextureSampler, float2(samplePos.x - blurDistance, samplePos.y - blurDistance)); - sample += tex2D(TextureSampler, float2(samplePos.x + blurDistance, samplePos.y - blurDistance)); - sample += tex2D(TextureSampler, float2(samplePos.x - blurDistance, samplePos.y + blurDistance)); + sample = xTexture.Sample(TextureSampler, float2(samplePos.x + blurDistance, samplePos.y + blurDistance)); + sample += xTexture.Sample(TextureSampler, float2(samplePos.x - blurDistance, samplePos.y - blurDistance)); + sample += xTexture.Sample(TextureSampler, float2(samplePos.x + blurDistance, samplePos.y - blurDistance)); + sample += xTexture.Sample(TextureSampler, float2(samplePos.x - blurDistance, samplePos.y + blurDistance)); sample = sample * 0.25f; return sample; } -float4 chromaticAberration(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 chromaticAberration(VertexShaderOutput input) : COLOR0 { - return float4(sampleWithChromaticAberration(texCoord), 1); + return float4(sampleWithChromaticAberration(input.TexCoords), 1); } -float4 chromaticAberrationDistort(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 chromaticAberrationDistort(VertexShaderOutput input) : COLOR0 { - float4 bumpColor = tex2D(DistortSampler, texCoord + distortUvOffset); - bumpColor = (bumpColor + tex2D(DistortSampler, texCoord - distortUvOffset * 2.0f)) * 0.5f; + float4 bumpColor = xDistortTexture.Sample(DistortSampler, input.TexCoords + distortUvOffset); + bumpColor = (bumpColor + xDistortTexture.Sample(DistortSampler, input.TexCoords - distortUvOffset * 2.0f)) * 0.5f; - float2 samplePos = texCoord; + float2 samplePos = input.TexCoords; samplePos.x += (bumpColor.r - 0.5f) * distortScale.x; samplePos.y += (bumpColor.g - 0.5f) * distortScale.y; @@ -117,11 +141,11 @@ float4 chromaticAberrationDistort(float4 position : SV_Position, float4 color : return float4(sampleWithChromaticAberration(samplePos), 1); } -float4 blurChromaticAberration(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 blurChromaticAberration(VertexShaderOutput input) : COLOR0 { - float2 samplePosR = radialDistortion(texCoord, chromaticAberrationStrength.r); - float2 samplePosG = radialDistortion(texCoord, chromaticAberrationStrength.g); - float2 samplePosB = radialDistortion(texCoord, chromaticAberrationStrength.b); + float2 samplePosR = radialDistortion(input.TexCoords, chromaticAberrationStrength.r); + float2 samplePosG = radialDistortion(input.TexCoords, chromaticAberrationStrength.g); + float2 samplePosB = radialDistortion(input.TexCoords, chromaticAberrationStrength.b); float2 blurTopLeft = -blurDistance; float2 blurTopRight = float2(blurDistance, -blurDistance); @@ -139,12 +163,12 @@ float4 blurChromaticAberration(float4 position : SV_Position, float4 color : COL return float4(sample, 1); } -float4 blurChromaticAberrationDistort(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 blurChromaticAberrationDistort(VertexShaderOutput input) : COLOR0 { - float4 bumpColor = tex2D(DistortSampler, texCoord + distortUvOffset); - bumpColor = (bumpColor + tex2D(DistortSampler, texCoord - distortUvOffset * 2.0f)) * 0.5f; + float4 bumpColor = xDistortTexture.Sample(DistortSampler, input.TexCoords + distortUvOffset); + bumpColor = (bumpColor + xDistortTexture.Sample(DistortSampler, input.TexCoords - distortUvOffset * 2.0f)) * 0.5f; - float2 samplePos = texCoord; + float2 samplePos = input.TexCoords; samplePos.x += (bumpColor.r - 0.5f) * distortScale.x; samplePos.y += (bumpColor.g - 0.5f) * distortScale.y; @@ -173,6 +197,7 @@ technique Distort { pass Pass1 { + VertexShader = compile vs_4_0_level_9_1 mainVS(); PixelShader = compile ps_4_0_level_9_1 distort(); } } @@ -181,6 +206,7 @@ technique Blur { pass Pass1 { + VertexShader = compile vs_4_0_level_9_1 mainVS(); PixelShader = compile ps_4_0_level_9_1 blur(); } } @@ -189,6 +215,7 @@ technique BlurDistort { pass Pass1 { + VertexShader = compile vs_4_0_level_9_1 mainVS(); PixelShader = compile ps_4_0_level_9_1 blurDistort(); } } @@ -197,6 +224,7 @@ technique BlurChromaticAberration { pass Pass1 { + VertexShader = compile vs_4_0_level_9_1 mainVS(); PixelShader = compile ps_4_0_level_9_1 blurChromaticAberration(); } } @@ -206,6 +234,7 @@ technique ChromaticAberration { pass Pass1 { + VertexShader = compile vs_4_0_level_9_1 mainVS(); PixelShader = compile ps_4_0_level_9_1 chromaticAberration(); } } @@ -214,6 +243,7 @@ technique ChromaticAberrationDistort { pass Pass1 { + VertexShader = compile vs_4_0_level_9_1 mainVS(); PixelShader = compile ps_4_0_level_9_1 chromaticAberrationDistort(); } } @@ -222,6 +252,7 @@ technique BlurChromaticAberrationDistort { pass Pass1 { + VertexShader = compile vs_4_0_level_9_1 mainVS(); PixelShader = compile ps_4_0_level_9_1 blurChromaticAberrationDistort(); } } diff --git a/Barotrauma/BarotraumaClient/Shaders/postprocess_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/postprocess_opengl.fx index 8941d42ef..063b2f107 100644 --- a/Barotrauma/BarotraumaClient/Shaders/postprocess_opengl.fx +++ b/Barotrauma/BarotraumaClient/Shaders/postprocess_opengl.fx @@ -1,7 +1,31 @@ -Texture2D xTexture; +struct VertexShaderInput +{ + float4 Position : POSITION0; + float2 TexCoords: TEXCOORD0; +}; + +struct VertexShaderOutput +{ + float4 Position : POSITION0; + float2 TexCoords: TEXCOORD0; +}; + +float4x4 MatrixTransform; + +VertexShaderOutput mainVS(in VertexShaderInput input) +{ + VertexShaderOutput output = (VertexShaderOutput)0; + + output.Position = mul(input.Position, MatrixTransform); + output.TexCoords = input.TexCoords; + + return output; +} + +Texture xTexture; sampler TextureSampler = sampler_state { Texture = ; }; -Texture2D xDistortTexture; +Texture xDistortTexture; sampler DistortSampler = sampler_state { @@ -53,24 +77,24 @@ float3 sampleWithChromaticAberration(float2 samplePos) tex2D(TextureSampler, radialDistortion(samplePos, chromaticAberrationStrength.b)).b); } -float4 blur(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 blur(VertexShaderOutput input) : COLOR0 { float4 sample; - sample = tex2D(TextureSampler, float2(texCoord.x + blurDistance, texCoord.y + blurDistance)); - sample += tex2D(TextureSampler, float2(texCoord.x - blurDistance, texCoord.y - blurDistance)); - sample += tex2D(TextureSampler, float2(texCoord.x + blurDistance, texCoord.y - blurDistance)); - sample += tex2D(TextureSampler, float2(texCoord.x - blurDistance, texCoord.y + blurDistance)); + sample = tex2D(TextureSampler, float2(input.TexCoords.x + blurDistance, input.TexCoords.y + blurDistance)); + sample += tex2D(TextureSampler, float2(input.TexCoords.x - blurDistance, input.TexCoords.y - blurDistance)); + sample += tex2D(TextureSampler, float2(input.TexCoords.x + blurDistance, input.TexCoords.y - blurDistance)); + sample += tex2D(TextureSampler, float2(input.TexCoords.x - blurDistance, input.TexCoords.y + blurDistance)); sample = sample * 0.25f; return sample; } -float4 distort(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 distort(VertexShaderOutput input) : COLOR0 { - float4 bumpColor = tex2D(DistortSampler, texCoord + distortUvOffset); - bumpColor = (bumpColor + tex2D(DistortSampler, texCoord - distortUvOffset * 2.0f)) * 0.5f; + float4 bumpColor = tex2D(DistortSampler, input.TexCoords + distortUvOffset); + bumpColor = (bumpColor + tex2D(DistortSampler, input.TexCoords - distortUvOffset * 2.0f)) * 0.5f; - float2 samplePos = texCoord; + float2 samplePos = input.TexCoords; samplePos.x += (bumpColor.r - 0.5f) * distortScale.x; samplePos.y += (bumpColor.g - 0.5f) * distortScale.y; @@ -78,12 +102,12 @@ float4 distort(float4 position : SV_Position, float4 color : COLOR0, float2 texC return tex2D(TextureSampler, samplePos); } -float4 blurDistort(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 blurDistort(VertexShaderOutput input) : COLOR0 { - float4 bumpColor = tex2D(DistortSampler, texCoord + distortUvOffset); - bumpColor = (bumpColor + tex2D(DistortSampler, texCoord - distortUvOffset * 2.0f)) * 0.5f; + float4 bumpColor = tex2D(DistortSampler, input.TexCoords + distortUvOffset); + bumpColor = (bumpColor + tex2D(DistortSampler, input.TexCoords - distortUvOffset * 2.0f)) * 0.5f; - float2 samplePos = texCoord; + float2 samplePos = input.TexCoords; samplePos.x += (bumpColor.r - 0.5f) * distortScale.x; samplePos.y += (bumpColor.g - 0.5f) * distortScale.y; @@ -99,17 +123,17 @@ float4 blurDistort(float4 position : SV_Position, float4 color : COLOR0, float2 return sample; } -float4 chromaticAberration(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 chromaticAberration(VertexShaderOutput input) : COLOR0 { - return float4(sampleWithChromaticAberration(texCoord), 1); + return float4(sampleWithChromaticAberration(input.TexCoords), 1); } -float4 chromaticAberrationDistort(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 chromaticAberrationDistort(VertexShaderOutput input) : COLOR0 { - float4 bumpColor = tex2D(DistortSampler, texCoord + distortUvOffset); - bumpColor = (bumpColor + tex2D(DistortSampler, texCoord - distortUvOffset * 2.0f)) * 0.5f; + float4 bumpColor = tex2D(DistortSampler, input.TexCoords + distortUvOffset); + bumpColor = (bumpColor + tex2D(DistortSampler, input.TexCoords - distortUvOffset * 2.0f)) * 0.5f; - float2 samplePos = texCoord; + float2 samplePos = input.TexCoords; samplePos.x += (bumpColor.r - 0.5f) * distortScale.x; samplePos.y += (bumpColor.g - 0.5f) * distortScale.y; @@ -117,11 +141,11 @@ float4 chromaticAberrationDistort(float4 position : SV_Position, float4 color : return float4(sampleWithChromaticAberration(samplePos), 1); } -float4 blurChromaticAberration(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 blurChromaticAberration(VertexShaderOutput input) : COLOR0 { - float2 samplePosR = radialDistortion(texCoord, chromaticAberrationStrength.r); - float2 samplePosG = radialDistortion(texCoord, chromaticAberrationStrength.g); - float2 samplePosB = radialDistortion(texCoord, chromaticAberrationStrength.b); + float2 samplePosR = radialDistortion(input.TexCoords, chromaticAberrationStrength.r); + float2 samplePosG = radialDistortion(input.TexCoords, chromaticAberrationStrength.g); + float2 samplePosB = radialDistortion(input.TexCoords, chromaticAberrationStrength.b); float2 blurTopLeft = -blurDistance; float2 blurTopRight = float2(blurDistance, -blurDistance); @@ -139,12 +163,12 @@ float4 blurChromaticAberration(float4 position : SV_Position, float4 color : COL return float4(sample, 1); } -float4 blurChromaticAberrationDistort(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 blurChromaticAberrationDistort(VertexShaderOutput input) : COLOR0 { - float4 bumpColor = tex2D(DistortSampler, texCoord + distortUvOffset); - bumpColor = (bumpColor + tex2D(DistortSampler, texCoord - distortUvOffset * 2.0f)) * 0.5f; + float4 bumpColor = tex2D(DistortSampler, input.TexCoords + distortUvOffset); + bumpColor = (bumpColor + tex2D(DistortSampler, input.TexCoords - distortUvOffset * 2.0f)) * 0.5f; - float2 samplePos = texCoord; + float2 samplePos = input.TexCoords; samplePos.x += (bumpColor.r - 0.5f) * distortScale.x; samplePos.y += (bumpColor.g - 0.5f) * distortScale.y; @@ -173,6 +197,7 @@ technique Distort { pass Pass1 { + VertexShader = compile vs_3_0 mainVS(); PixelShader = compile ps_3_0 distort(); } } @@ -181,6 +206,7 @@ technique Blur { pass Pass1 { + VertexShader = compile vs_3_0 mainVS(); PixelShader = compile ps_3_0 blur(); } } @@ -189,6 +215,7 @@ technique BlurDistort { pass Pass1 { + VertexShader = compile vs_3_0 mainVS(); PixelShader = compile ps_3_0 blurDistort(); } } @@ -197,6 +224,7 @@ technique BlurChromaticAberration { pass Pass1 { + VertexShader = compile vs_3_0 mainVS(); PixelShader = compile ps_3_0 blurChromaticAberration(); } } @@ -206,6 +234,7 @@ technique ChromaticAberration { pass Pass1 { + VertexShader = compile vs_3_0 mainVS(); PixelShader = compile ps_3_0 chromaticAberration(); } } @@ -214,6 +243,7 @@ technique ChromaticAberrationDistort { pass Pass1 { + VertexShader = compile vs_3_0 mainVS(); PixelShader = compile ps_3_0 chromaticAberrationDistort(); } } @@ -222,6 +252,7 @@ technique BlurChromaticAberrationDistort { pass Pass1 { + VertexShader = compile vs_3_0 mainVS(); PixelShader = compile ps_3_0 blurChromaticAberrationDistort(); } } diff --git a/Barotrauma/BarotraumaClient/Shaders/solidcolor.fx b/Barotrauma/BarotraumaClient/Shaders/solidcolor.fx index 846f3d2fe..f1fc3c52b 100644 --- a/Barotrauma/BarotraumaClient/Shaders/solidcolor.fx +++ b/Barotrauma/BarotraumaClient/Shaders/solidcolor.fx @@ -4,13 +4,13 @@ sampler TextureSampler = sampler_state { Texture = ; }; float blurDistance; float4 color; -float4 solidColor(float4 position : SV_Position, float4 clr : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 solidColor(float4 position : POSITION0, float4 clr : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { float a = tex2D(TextureSampler, texCoord).a; return color * a; } -float4 solidColorBlur(float4 position : SV_Position, float4 clr : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 solidColorBlur(float4 position : POSITION0, float4 clr : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { float sample; sample = tex2D(TextureSampler, float2(texCoord.x + blurDistance, texCoord.y + blurDistance)).a; diff --git a/Barotrauma/BarotraumaClient/Shaders/solidcolor_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/solidcolor_opengl.fx index 2c82b0bf3..f62aeda9e 100644 --- a/Barotrauma/BarotraumaClient/Shaders/solidcolor_opengl.fx +++ b/Barotrauma/BarotraumaClient/Shaders/solidcolor_opengl.fx @@ -4,13 +4,13 @@ sampler TextureSampler = sampler_state { Texture = ; }; float blurDistance; float4 color; -float4 solidColor(float4 position : SV_Position, float4 clr : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 solidColor(float4 position : POSITION0, float4 clr : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { float a = tex2D(TextureSampler, texCoord).a; return color * a; } -float4 solidColorBlur(float4 position : SV_Position, float4 clr : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 solidColorBlur(float4 position : POSITION0, float4 clr : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { float sample; sample = tex2D(TextureSampler, float2(texCoord.x + blurDistance, texCoord.y + blurDistance)).a; diff --git a/Barotrauma/BarotraumaClient/Shaders/watershader.fx b/Barotrauma/BarotraumaClient/Shaders/watershader.fx index 331e860ec..a8f053ed2 100644 --- a/Barotrauma/BarotraumaClient/Shaders/watershader.fx +++ b/Barotrauma/BarotraumaClient/Shaders/watershader.fx @@ -20,14 +20,14 @@ float4x4 xUvTransform; struct VertexShaderInput { - float4 Position : SV_POSITION; + float4 Position : POSITION0; float4 Color : COLOR0; float2 TexCoords: TEXCOORD0; // added }; struct VertexShaderOutput { - float4 Position : SV_POSITION; + float4 Position : POSITION0; float4 Color : COLOR0; float2 TexCoords: TEXCOORD0; // added }; @@ -95,7 +95,7 @@ float4 mainPSBlurred(VertexShaderOutput input) : COLOR return sample; } -float4 mainPostProcess(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 mainPostProcess(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { float4 bumpColor = tex2D(WaterBumpSampler, texCoord + xUvOffset + xBumpPos); bumpColor = (bumpColor + tex2D(WaterBumpSampler, texCoord - xUvOffset * 2.0f + xBumpPos)) * 0.5f; diff --git a/Barotrauma/BarotraumaClient/Shaders/watershader_opengl.fx b/Barotrauma/BarotraumaClient/Shaders/watershader_opengl.fx index ddef6b01e..842f5b201 100644 --- a/Barotrauma/BarotraumaClient/Shaders/watershader_opengl.fx +++ b/Barotrauma/BarotraumaClient/Shaders/watershader_opengl.fx @@ -20,14 +20,14 @@ float4x4 xUvTransform; struct VertexShaderInput { - float4 Position : SV_POSITION; + float4 Position : POSITION0; float4 Color : COLOR0; float2 TexCoords: TEXCOORD0; // added }; struct VertexShaderOutput { - float4 Position : SV_POSITION; + float4 Position : POSITION0; float4 Color : COLOR0; float2 TexCoords: TEXCOORD0; // added }; @@ -102,7 +102,7 @@ float4 mainPSBlurred(VertexShaderOutput input) : COLOR return sample; } -float4 mainPostProcess(float4 position : SV_Position, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 +float4 mainPostProcess(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { float4 bumpColor = tex2D(WaterBumpSampler, texCoord + xUvOffset + xBumpPos); bumpColor = (bumpColor + tex2D(WaterBumpSampler, texCoord - xUvOffset * 2.0f + xBumpPos)) * 0.5f; diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 78bd010ca..4642e0645 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.9.8.0 + 0.9.9.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index e7c7a486a..e6e084590 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.9.8.0 + 0.9.9.0 Copyright © FakeFish 2018-2020 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index dfb11cdcc..eda82c727 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.9.8.0 + 0.9.9.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 fc7254410..fed973a06 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/Character.cs @@ -9,20 +9,23 @@ namespace Barotrauma partial void InitProjSpecific(XElement mainElement) { } - partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult) + partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult, float stun) { - GameMain.Server.KarmaManager.OnCharacterHealthChanged(this, attacker, attackResult.Damage, attackResult.Afflictions); + GameMain.Server.KarmaManager.OnCharacterHealthChanged(this, attacker, attackResult.Damage, stun, attackResult.Afflictions); } - partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction) + partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool log) { - if (causeOfDeath == CauseOfDeathType.Affliction) + if (log) { - GameServer.Log(LogName + " has died (Cause of death: " + causeOfDeathAffliction.Prefab.Name + ")", ServerLog.MessageType.Attack); - } - else - { - GameServer.Log(LogName + " has died (Cause of death: " + causeOfDeath + ")", ServerLog.MessageType.Attack); + if (causeOfDeath == CauseOfDeathType.Affliction) + { + GameServer.Log(GameServer.CharacterLogName(this) + " has died (Cause of death: " + causeOfDeathAffliction.Prefab.Name + ")", ServerLog.MessageType.Attack); + } + else + { + GameServer.Log(GameServer.CharacterLogName(this) + " has died (Cause of death: " + causeOfDeath + ")", ServerLog.MessageType.Attack); + } } healthUpdateTimer = 0.0f; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index 2e650759b..28b09f25e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -243,7 +243,7 @@ namespace Barotrauma return; } - if (IsUnconscious) + if (IsIncapacitated) { var causeOfDeath = CharacterHealth.GetCauseOfDeath(); Kill(causeOfDeath.First, causeOfDeath.Second); @@ -264,21 +264,21 @@ namespace Barotrauma switch ((NetEntityEvent.Type)extraData[0]) { case NetEntityEvent.Type.InventoryState: - msg.WriteRangedInteger(0, 0, 3); + msg.WriteRangedInteger(0, 0, 4); msg.Write(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0); Inventory.ServerWrite(msg, c); break; case NetEntityEvent.Type.Control: - msg.WriteRangedInteger(1, 0, 3); + msg.WriteRangedInteger(1, 0, 4); Client owner = (Client)extraData[1]; msg.Write(owner != null && owner.Character == this && GameMain.Server.ConnectedClients.Contains(owner) ? owner.ID : (byte)0); break; case NetEntityEvent.Type.Status: - msg.WriteRangedInteger(2, 0, 3); + msg.WriteRangedInteger(2, 0, 4); WriteStatus(msg); break; case NetEntityEvent.Type.UpdateSkills: - msg.WriteRangedInteger(3, 0, 3); + msg.WriteRangedInteger(3, 0, 4); if (Info?.Job == null) { msg.Write((byte)0); @@ -293,6 +293,15 @@ namespace Barotrauma } } break; + case NetEntityEvent.Type.ExecuteAttack: + Limb attackLimb = extraData[1] as Limb; + UInt16 targetEntityID = (UInt16)extraData[2]; + int targetLimbIndex = extraData.Length > 3 ? (int)extraData[3] : 0; + msg.WriteRangedInteger(4, 0, 4); + msg.Write((byte)(Removed ? 0 : Array.IndexOf(AnimController.Limbs, attackLimb))); + msg.Write(targetEntityID); + msg.Write((byte)targetLimbIndex); + break; default: DebugConsole.ThrowError("Invalid NetworkEvent type for entity " + ToString() + " (" + (NetEntityEvent.Type)extraData[0] + ")"); break; @@ -354,7 +363,7 @@ namespace Barotrauma Vector2 relativeCursorPos = cursorPosition - AimRefPosition; tempBuffer.Write((UInt16)(65535.0 * Math.Atan2(relativeCursorPos.Y, relativeCursorPos.X) / (2.0 * Math.PI))); - tempBuffer.Write(IsRagdolled || IsUnconscious || Stun > 0.0f || IsDead); + tempBuffer.Write(IsRagdolled || Stun > 0.0f || IsDead || IsIncapacitated); tempBuffer.Write(AnimController.Dir > 0.0f); } @@ -497,6 +506,31 @@ namespace Barotrauma msg.Write(this is AICharacter); msg.Write(info.SpeciesName); info.ServerWrite(msg); + + // Current order + if (info.CurrentOrder != null) + { + msg.Write(true); + msg.Write((byte)Order.PrefabList.IndexOf(info.CurrentOrder.Prefab)); + msg.Write(info.CurrentOrder.TargetEntity == null ? (UInt16)0 : + info.CurrentOrder.TargetEntity.ID); + if (info.CurrentOrder.OrderGiver != null) + { + msg.Write(true); + msg.Write(info.CurrentOrder.OrderGiver.ID); + } + else + { + msg.Write(false); + } + msg.Write((byte)(string.IsNullOrWhiteSpace(info.CurrentOrderOption) ? 0 : + Array.IndexOf(info.CurrentOrder.Prefab.Options, info.CurrentOrderOption))); + } + else + { + msg.Write(false); + } + TryWriteStatus(msg); void TryWriteStatus(IWriteMessage msg) diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index b4ed048c7..58d82a5c9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -82,10 +82,6 @@ namespace Barotrauma { if (queuedMessages.Count > 0) { - int inputLines = Math.Max((int)Math.Ceiling(input.Length / (float)Console.WindowWidth), 1); - Console.CursorLeft = 0; - Console.Write(new string(' ', consoleWidth)); - Console.CursorTop = Math.Max(Console.CursorTop - inputLines, 0); Console.CursorLeft = 0; while (queuedMessages.Count > 0) { @@ -192,32 +188,64 @@ namespace Barotrauma sw.Stop(); } + private static void WriteAndResetLine(string txt) + { + int consoleWidth = Console.BufferWidth; + int linesWritten = 0; + while (true) + { + if (txt.Length > consoleWidth) + { + linesWritten++; + Console.Write(txt.Substring(0, consoleWidth)); + txt = txt.Substring(consoleWidth); + } + else + { + Console.Write(txt); + if (txt.Length == consoleWidth) + { + Console.Write(' '); Console.CursorLeft--; + linesWritten++; + } + break; + } + } + Console.CursorTop -= linesWritten; + } + private static void RewriteInputToCommandLine(string input) { if (Console.WindowWidth == 0 || Console.WindowHeight == 0) { return; } - int consoleWidth = Math.Max(Console.WindowWidth, 5); - int inputLines = Math.Max((int)Math.Ceiling(input.Length / (float)consoleWidth), 1); - int cursorLine = Math.Max((int)Math.Ceiling((input.Length + 1) / (float)consoleWidth), 1); + int consoleWidth = Math.Max(Console.BufferWidth, 5); + //int inputLines = Math.Max((int)Math.Ceiling(input.Length / (float)consoleWidth), 1); + //int cursorLine = Math.Max((int)Math.Ceiling((input.Length + 1) / (float)consoleWidth), 1); try { - Console.WriteLine(""); Console.CursorTop -= inputLines; - + string tmpInput = input; + while (tmpInput.Length >= consoleWidth) + { + tmpInput = tmpInput.Substring(consoleWidth); + } string ln = input.Length > 0 ? AutoComplete(input, 0) : ""; + while (ln.Length >= consoleWidth) + { + ln = ln.Substring(consoleWidth); + } ln += new string(' ', consoleWidth - (ln.Length % consoleWidth)); Console.ForegroundColor = ConsoleColor.DarkGray; Console.CursorLeft = 0; - Console.Write(ln); + WriteAndResetLine(ln); Console.ForegroundColor = ConsoleColor.White; Console.CursorLeft = 0; - Console.CursorTop -= cursorLine; - Console.Write(input); + WriteAndResetLine(tmpInput); Console.CursorLeft = input.Length % consoleWidth; } catch (Exception e) { - string errorMsg = "Failed to write input to command line (window width: " + Console.WindowWidth + ", window height: " + Console.WindowHeight + ", inputLines:" + inputLines + ")\n" + string errorMsg = "Failed to write input to command line (window width: " + Console.WindowWidth + ", window height: " + Console.WindowHeight + ")\n" + e.Message + "\n" + e.StackTrace; GameAnalyticsManager.AddErrorEventOnce("DebugConsole.RewriteInputToCommandLine", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); } @@ -558,7 +586,7 @@ namespace Barotrauma ShowQuestionPrompt("Rank to grant to \"" + client.Name + "\"?", (rank) => { - PermissionPreset preset = PermissionPreset.List.Find(p => p.Name.ToLowerInvariant() == rank.ToLowerInvariant()); + PermissionPreset preset = PermissionPreset.List.Find(p => p.Name.Equals(rank, StringComparison.OrdinalIgnoreCase)); if (preset == null) { ThrowError("Rank \"" + rank + "\" not found."); @@ -948,7 +976,7 @@ namespace Barotrauma NewMessage("***************", Color.Cyan); foreach (Client c in GameMain.Server.ConnectedClients) { - NewMessage("- " + c.ID.ToString() + ": " + c.Name + (c.Character != null ? " playing " + c.Character.LogName : "") + ", " + c.Connection.EndPointString, Color.Cyan); + NewMessage("- " + c.ID.ToString() + ": " + c.Name + (c.Character != null ? " playing " + c.Character.LogName : "") + ", " + c.Connection.EndPointString + $", ping {c.Ping} ms", Color.Cyan); } NewMessage("***************", Color.Cyan); })); @@ -957,7 +985,7 @@ namespace Barotrauma GameMain.Server.SendConsoleMessage("***************", client); foreach (Client c in GameMain.Server.ConnectedClients) { - GameMain.Server.SendConsoleMessage("- " + c.ID.ToString() + ": " + c.Name + ", " + c.Connection.EndPointString, client); + GameMain.Server.SendConsoleMessage("- " + c.ID.ToString() + ": " + c.Name + ", " + c.Connection.EndPointString + $", ping {c.Ping} ms", client); } GameMain.Server.SendConsoleMessage("***************", client); }); @@ -1165,7 +1193,7 @@ namespace Barotrauma else { string modeName = string.Join(" ", args); - if (modeName.ToLowerInvariant() == "campaign") + if (modeName.Equals("campaign", StringComparison.OrdinalIgnoreCase)) { MultiPlayerCampaign.StartCampaignSetup(); } @@ -1210,7 +1238,7 @@ namespace Barotrauma commands.Add(new Command("sub|submarine", "submarine [name]: Select the submarine for the next round.", (string[] args) => { - Submarine sub = GameMain.NetLobbyScreen.GetSubList().Find(s => s.Name.ToLower() == string.Join(" ", args).ToLower()); + SubmarineInfo sub = GameMain.NetLobbyScreen.GetSubList().Find(s => s.Name.ToLower() == string.Join(" ", args).ToLower()); if (sub != null) { @@ -1223,13 +1251,13 @@ namespace Barotrauma { return new string[][] { - Submarine.SavedSubmarines.Select(s => s.Name).ToArray() + SubmarineInfo.SavedSubmarines.Select(s => s.Name).ToArray() }; })); commands.Add(new Command("shuttle", "shuttle [name]: Select the specified submarine as the respawn shuttle for the next round.", (string[] args) => { - Submarine shuttle = GameMain.NetLobbyScreen.GetSubList().Find(s => s.Name.ToLower() == string.Join(" ", args).ToLower()); + SubmarineInfo shuttle = GameMain.NetLobbyScreen.GetSubList().Find(s => s.Name.ToLower() == string.Join(" ", args).ToLower()); if (shuttle != null) { @@ -1242,7 +1270,7 @@ namespace Barotrauma { return new string[][] { - Submarine.SavedSubmarines.Select(s => s.Name).ToArray() + SubmarineInfo.SavedSubmarines.Select(s => s.Name).ToArray() }; })); @@ -1475,6 +1503,27 @@ namespace Barotrauma } ); + AssignOnClientRequestExecute( + "teleportsub", + (Client client, Vector2 cursorWorldPos, string[] args) => + { + if (Submarine.MainSub == null || Level.Loaded == null) return; + + if (args.Length == 0 || args[0].Equals("cursor", StringComparison.OrdinalIgnoreCase)) + { + Submarine.MainSub.SetPosition(cursorWorldPos); + } + else if (args[0].Equals("start", StringComparison.OrdinalIgnoreCase)) + { + Submarine.MainSub.SetPosition(Level.Loaded.StartPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height); + } + else + { + Submarine.MainSub.SetPosition(Level.Loaded.EndPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height); + } + } + ); + AssignOnClientRequestExecute( "godmode", (Client client, Vector2 cursorWorldPos, string[] args) => @@ -1493,7 +1542,7 @@ namespace Barotrauma { if (args.Length < 2) return; - AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a => a.Name.ToLowerInvariant() == args[0].ToLowerInvariant()); + AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a => a.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase)); if (afflictionPrefab == null) { GameMain.Server.SendConsoleMessage("Affliction \"" + args[0] + "\" not found.", client); @@ -1576,13 +1625,14 @@ namespace Barotrauma (Client client, Vector2 cursorWorldPos, string[] args) => { Vector2 explosionPos = cursorWorldPos; - float range = 500, force = 10, damage = 50, structureDamage = 10, empStrength = 0.0f; ; + float range = 500, force = 10, damage = 50, structureDamage = 10, itemDamage = 100, empStrength = 0.0f; ; if (args.Length > 0) float.TryParse(args[0], out range); if (args.Length > 1) float.TryParse(args[1], out force); if (args.Length > 2) float.TryParse(args[2], out damage); if (args.Length > 3) float.TryParse(args[3], out structureDamage); - if (args.Length > 4) float.TryParse(args[4], out empStrength); - new Explosion(range, force, damage, structureDamage, empStrength).Explode(explosionPos, null); + if (args.Length > 4) float.TryParse(args[4], out itemDamage); + if (args.Length > 5) float.TryParse(args[5], out empStrength); + new Explosion(range, force, damage, structureDamage, itemDamage, empStrength).Explode(explosionPos, null); } ); @@ -1718,7 +1768,7 @@ namespace Barotrauma } string rank = string.Join("", args.Skip(1)); - PermissionPreset preset = PermissionPreset.List.Find(p => p.Name.ToLowerInvariant() == rank.ToLowerInvariant()); + PermissionPreset preset = PermissionPreset.List.Find(p => p.Name.Equals(rank, StringComparison.OrdinalIgnoreCase)); if (preset == null) { GameMain.Server.SendConsoleMessage("Rank \"" + rank + "\" not found.", senderClient); @@ -1966,7 +2016,7 @@ namespace Barotrauma if (!client.HasPermission(ClientPermissions.ConsoleCommands) && client.Connection != GameMain.Server.OwnerConnection) { GameMain.Server.SendConsoleMessage("You are not permitted to use console commands!", client); - GameServer.Log(client.Name + " attempted to execute the console command \"" + command + "\" without a permission to use console commands.", ServerLog.MessageType.ConsoleUsage); + GameServer.Log(GameServer.ClientLogName(client) + " attempted to execute the console command \"" + command + "\" without a permission to use console commands.", ServerLog.MessageType.ConsoleUsage); return; } @@ -1975,7 +2025,7 @@ namespace Barotrauma if (matchingCommand != null && !client.PermittedConsoleCommands.Contains(matchingCommand) && client.Connection != GameMain.Server.OwnerConnection) { GameMain.Server.SendConsoleMessage("You are not permitted to use the command\"" + matchingCommand.names[0] + "\"!", client); - GameServer.Log(client.Name + " attempted to execute the console command \"" + command + "\" without a permission to use the command.", ServerLog.MessageType.ConsoleUsage); + GameServer.Log(GameServer.ClientLogName(client) + " attempted to execute the console command \"" + command + "\" without a permission to use the command.", ServerLog.MessageType.ConsoleUsage); return; } else if (matchingCommand == null) @@ -1987,18 +2037,18 @@ namespace Barotrauma if (!MathUtils.IsValid(cursorWorldPos)) { GameMain.Server.SendConsoleMessage("Could not execute command \"" + command + "\" - invalid cursor position.", client); - NewMessage(client.Name + " attempted to execute the console command \"" + command + "\" with invalid cursor position.", Color.White); + NewMessage(GameServer.ClientLogName(client) + " attempted to execute the console command \"" + command + "\" with invalid cursor position.", Color.White); return; } try { matchingCommand.ServerExecuteOnClientRequest(client, cursorWorldPos, splitCommand.Skip(1).ToArray()); - GameServer.Log("Console command \"" + command + "\" executed by " + client.Name + ".", ServerLog.MessageType.ConsoleUsage); + GameServer.Log("Console command \"" + command + "\" executed by " + GameServer.ClientLogName(client) + ".", ServerLog.MessageType.ConsoleUsage); } catch (Exception e) { - ThrowError("Executing the command \"" + matchingCommand.names[0] + "\" by request from \"" + client.Name + "\" failed.", e); + ThrowError("Executing the command \"" + matchingCommand.names[0] + "\" by request from \"" + GameServer.ClientLogName(client) + "\" failed.", e); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CargoMission.cs index 7c3cd5b32..9ee80f0f8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CargoMission.cs @@ -9,7 +9,7 @@ namespace Barotrauma msg.Write((ushort)items.Count); foreach (Item item in items) { - item.WriteSpawnData(msg, item.ID); + item.WriteSpawnData(msg, item.ID, item.ParentInventory?.Owner?.ID ?? 0); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs index 5c4f447f0..170b5b538 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/CombatMission.cs @@ -75,8 +75,8 @@ namespace Barotrauma } else { - teamDead[0] = crews[0].All(c => c.IsDead || c.IsUnconscious); - teamDead[1] = crews[1].All(c => c.IsDead || c.IsUnconscious); + teamDead[0] = crews[0].All(c => c.IsDead || c.IsIncapacitated); + teamDead[1] = crews[1].All(c => c.IsDead || c.IsIncapacitated); } if (state == 0) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs index 09a323ee8..1ceed8427 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Events/Missions/SalvageMission.cs @@ -1,12 +1,32 @@ using Barotrauma.Networking; +using System.Collections.Generic; namespace Barotrauma { - partial class SalvageMission : Mission + partial class SalvageMission : Mission { + private bool usedExistingItem; + + private readonly List> executedEffectIndices = new List>(); + public override void ServerWriteInitial(IWriteMessage msg, Client c) { - item.WriteSpawnData(msg, item.ID); + msg.Write(usedExistingItem); + if (usedExistingItem) + { + msg.Write(item.ID); + } + else + { + item.WriteSpawnData(msg, item.ID, item.ParentInventory?.Owner?.ID ?? 0); + } + + msg.Write((byte)executedEffectIndices.Count); + foreach (Pair effectIndex in executedEffectIndices) + { + msg.Write((byte)effectIndex.First); + msg.Write((byte)effectIndex.Second); + } } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index fe6bc2504..3ce575eb9 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -61,7 +61,7 @@ namespace Barotrauma if (vanillaContent == null) { // TODO: Dynamic method for defining and finding the vanilla content package. - vanillaContent = ContentPackage.List.SingleOrDefault(cp => Path.GetFileName(cp.Path).ToLowerInvariant() == "vanilla 0.9.xml"); + vanillaContent = ContentPackage.List.SingleOrDefault(cp => Path.GetFileName(cp.Path).Equals("vanilla 0.9.xml", StringComparison.OrdinalIgnoreCase)); } return vanillaContent; } @@ -111,6 +111,7 @@ namespace Barotrauma StructurePrefab.LoadAll(GetFilesOfType(ContentType.Structure)); ItemPrefab.LoadAll(GetFilesOfType(ContentType.Item)); JobPrefab.LoadAll(GetFilesOfType(ContentType.Jobs)); + CorpsePrefab.LoadAll(GetFilesOfType(ContentType.Corpses)); NPCConversation.LoadAll(GetFilesOfType(ContentType.NPCConversations)); ItemAssemblyPrefab.LoadAll(); LevelObjectPrefab.LoadAll(); @@ -118,7 +119,7 @@ namespace Barotrauma GameModePreset.Init(); LocationType.Init(); - Submarine.RefreshSavedSubs(); + SubmarineInfo.RefreshSavedSubs(); Screen.SelectNull(); diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index caff80bd3..0d321ad08 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -15,7 +15,7 @@ namespace Barotrauma { if (string.IsNullOrWhiteSpace(savePath)) return; - GameMain.GameSession = new GameSession(new Submarine(subPath, ""), savePath, + GameMain.GameSession = new GameSession(new SubmarineInfo(subPath, ""), savePath, GameModePreset.List.Find(g => g.Identifier == "multiplayercampaign")); var campaign = ((MultiPlayerCampaign)GameMain.GameSession.GameMode); campaign.GenerateMap(seed); @@ -46,7 +46,7 @@ namespace Barotrauma DebugConsole.NewMessage("********* CAMPAIGN SETUP *********", Color.White); DebugConsole.ShowQuestionPrompt("Do you want to start a new campaign? Y/N", (string arg) => { - if (arg.ToLowerInvariant() == "y" || arg.ToLowerInvariant() == "yes") + if (arg.Equals("y", StringComparison.OrdinalIgnoreCase) || arg.Equals("yes", StringComparison.OrdinalIgnoreCase)) { DebugConsole.ShowQuestionPrompt("Enter a save name for the campaign:", (string saveName) => { @@ -189,7 +189,7 @@ namespace Barotrauma foreach (PurchasedItem pi in CargoManager.PurchasedItems) { msg.Write(pi.ItemPrefab.Identifier); - msg.Write((UInt16)pi.Quantity); + msg.WriteRangedInteger(pi.Quantity, 0, 100); } var characterData = GetClientCharacterData(c); @@ -217,7 +217,7 @@ namespace Barotrauma for (int i = 0; i < purchasedItemCount; i++) { string itemPrefabIdentifier = msg.ReadString(); - UInt16 itemQuantity = msg.ReadUInt16(); + int itemQuantity = msg.ReadRangedInteger(0, CargoManager.MaxQuantity); purchasedItems.Add(new PurchasedItem(ItemPrefab.Prefabs[itemPrefabIdentifier], itemQuantity)); } @@ -255,8 +255,8 @@ namespace Barotrauma } if (purchasedLostShuttles != this.PurchasedLostShuttles) { - if (GameMain.GameSession?.Submarine != null && - GameMain.GameSession.Submarine.LeftBehindSubDockingPortOccupied) + if (GameMain.GameSession?.SubmarineInfo != null && + GameMain.GameSession.SubmarineInfo.LeftBehindSubDockingPortOccupied) { GameMain.Server.SendDirectChatMessage(TextManager.FormatServerMessage("ReplaceShuttleDockingPortOccupied"), sender, ChatMessageType.MessageBox); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs index 4ad1b84cd..4b6f10240 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Holdable/Holdable.cs @@ -30,7 +30,7 @@ namespace Barotrauma.Items.Components AttachToWall(); item.CreateServerEvent(this); - GameServer.Log(c.Character.LogName + " attached " + item.Name + " to a wall", ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(c.Character) + " attached " + item.Name + " to a wall", ServerLog.MessageType.ItemInteraction); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/LightComponent.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/LightComponent.cs new file mode 100644 index 000000000..d7331f33b --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/LightComponent.cs @@ -0,0 +1,45 @@ +using Barotrauma.Networking; +using System.Collections.Generic; + +namespace Barotrauma.Items.Components +{ + partial class LightComponent : Powered, IServerSerializable + { + private CoroutineHandle sendStateCoroutine; + private bool lastSentState; + private float sendStateTimer; + + partial void OnStateChanged() + { + sendStateTimer = 0.5f; + if (sendStateCoroutine == null) + { + sendStateCoroutine = CoroutineManager.StartCoroutine(SendStateAfterDelay()); + } + } + + private IEnumerable SendStateAfterDelay() + { + while (sendStateTimer > 0.0f) + { + sendStateTimer -= CoroutineManager.DeltaTime; + yield return CoroutineStatus.Running; + } + + if (item.Removed || GameMain.NetworkMember == null) + { + yield return CoroutineStatus.Success; + } + + sendStateCoroutine = null; + if (lastSentState != IsActive) { item.CreateServerEvent(this); } + yield return CoroutineStatus.Success; + } + + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + { + msg.Write(IsActive); + lastSentState = IsActive; + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Engine.cs index af8583204..d39ad733f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Engine.cs @@ -19,7 +19,7 @@ namespace Barotrauma.Items.Components { if (Math.Abs(newTargetForce - targetForce) > 0.01f) { - GameServer.Log(c.Character.LogName + " set the force of " + item.Name + " to " + (int)(newTargetForce) + " %", ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(c.Character) + " set the force of " + item.Name + " to " + (int)(newTargetForce) + " %", ServerLog.MessageType.ItemInteraction); } targetForce = newTargetForce; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs index 4b8261204..903b05330 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Machines/Pump.cs @@ -17,11 +17,11 @@ namespace Barotrauma.Items.Components { if (newFlowPercentage != FlowPercentage) { - GameServer.Log(c.Character.LogName + " set the pumping speed of " + item.Name + " to " + (int)(newFlowPercentage) + " %", ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(c.Character) + " set the pumping speed of " + item.Name + " to " + (int)(newFlowPercentage) + " %", ServerLog.MessageType.ItemInteraction); } if (newIsActive != IsActive) { - GameServer.Log(c.Character.LogName + (newIsActive ? " turned on " : " turned off ") + item.Name, ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(c.Character) + (newIsActive ? " turned on " : " turned off ") + item.Name, ServerLog.MessageType.ItemInteraction); } FlowPercentage = newFlowPercentage; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Power/PowerContainer.cs index 8b9acaaf8..20ae5b275 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Power/PowerContainer.cs @@ -14,7 +14,7 @@ namespace Barotrauma.Items.Components if (item.CanClientAccess(c)) { RechargeSpeed = newRechargeSpeed; - GameServer.Log(c.Character.LogName + " set the recharge speed of " + item.Name + " to " + (int)((rechargeSpeed / maxRechargeSpeed) * 100.0f) + " %", ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(c.Character) + " set the recharge speed of " + item.Name + " to " + (int)((rechargeSpeed / maxRechargeSpeed) * 100.0f) + " %", ServerLog.MessageType.ItemInteraction); } item.CreateServerEvent(this); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs new file mode 100644 index 000000000..bc69285de --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Projectile.cs @@ -0,0 +1,42 @@ +using Barotrauma.Networking; +using System; + +namespace Barotrauma.Items.Components +{ + partial class Projectile : ItemComponent + { + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + { + bool stuck = StickTarget != null && !item.Removed && !StickTargetRemoved(); + msg.Write(stuck); + if (stuck) + { + msg.Write(item.Submarine?.ID ?? Entity.NullEntityID); + msg.Write(item.CurrentHull?.ID ?? Entity.NullEntityID); + msg.Write(item.SimPosition.X); + msg.Write(item.SimPosition.Y); + msg.Write(stickJoint.Axis.X); + msg.Write(stickJoint.Axis.Y); + if (StickTarget.UserData is Structure structure) + { + msg.Write(structure.ID); + int bodyIndex = structure.Bodies.IndexOf(StickTarget); + msg.Write((byte)(bodyIndex == -1 ? 0 : bodyIndex)); + } + else if (StickTarget.UserData is Entity entity) + { + msg.Write(entity.ID); + } + else if (StickTarget.UserData is Limb limb) + { + msg.Write(limb.character.ID); + msg.Write((byte)Array.IndexOf(limb.character.AnimController.Limbs, limb)); + } + else + { + throw new NotImplementedException(StickTarget.UserData?.ToString() ?? "null" + " is not a valid projectile stick target."); + } + } + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs index 43b056872..3cf56ec69 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Repairable.cs @@ -12,7 +12,7 @@ namespace Barotrauma.Items.Components public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) { - if (c.Character == null) return; + if (c.Character == null) { return; } var requestedFixAction = (FixActions)msg.ReadRangedInteger(0, 2); if (requestedFixAction != FixActions.None) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Rope.cs new file mode 100644 index 000000000..16e3abf9a --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Rope.cs @@ -0,0 +1,12 @@ +using Barotrauma.Networking; + +namespace Barotrauma.Items.Components +{ + partial class Rope : ItemComponent + { + public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) + { + msg.Write(Snapped); + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs index e11921678..d29f38b98 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/ConnectionPanel.cs @@ -66,6 +66,9 @@ namespace Barotrauma.Items.Components if (!CheckCharacterSuccess(c.Character)) { + item.CreateServerEvent(this); + c.Character.SelectedItems[0]?.GetComponent()?.CreateNetworkEvent(); + c.Character.SelectedItems[1]?.GetComponent()?.CreateNetworkEvent(); GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFailure, this, c.Character.ID }); return; } @@ -85,7 +88,7 @@ namespace Barotrauma.Items.Components if (existingWire.Locked) { //this should not be possible unless the client is running a modified version of the game - GameServer.Log(c.Character.LogName + " attempted to disconnect a locked wire from " + + GameServer.Log(GameServer.CharacterLogName(c.Character) + " attempted to disconnect a locked wire from " + Connections[i].Item.Name + " (" + Connections[i].Name + ")", ServerLog.MessageType.Error); continue; } @@ -103,7 +106,7 @@ namespace Barotrauma.Items.Components if (existingWire.Connections[0] == null && existingWire.Connections[1] == null) { - GameServer.Log(c.Character.LogName + " disconnected a wire from " + + GameServer.Log(GameServer.CharacterLogName(c.Character) + " disconnected a wire from " + Connections[i].Item.Name + " (" + Connections[i].Name + ")", ServerLog.MessageType.Wiring); if (existingWire.Item.ParentInventory != null) @@ -119,7 +122,7 @@ namespace Barotrauma.Items.Components } else if (existingWire.Connections[0] != null) { - GameServer.Log(c.Character.LogName + " disconnected a wire from " + + GameServer.Log(GameServer.CharacterLogName(c.Character) + " disconnected a wire from " + Connections[i].Item.Name + " (" + Connections[i].Name + ") to " + existingWire.Connections[0].Item.Name + " (" + existingWire.Connections[0].Name + ")", ServerLog.MessageType.Wiring); //wires that are not in anyone's inventory (i.e. not currently being rewired) @@ -134,7 +137,7 @@ namespace Barotrauma.Items.Components } else if (existingWire.Connections[1] != null) { - GameServer.Log(c.Character.LogName + " disconnected a wire from " + + GameServer.Log(GameServer.CharacterLogName(c.Character) + " disconnected a wire from " + Connections[i].Item.Name + " (" + Connections[i].Name + ") to " + existingWire.Connections[1].Item.Name + " (" + existingWire.Connections[1].Name + ")", ServerLog.MessageType.Wiring); /*if (existingWire.Item.ParentInventory == null && !wires.Any(w => w.Contains(existingWire))) @@ -158,7 +161,7 @@ namespace Barotrauma.Items.Components disconnectedWire.Item.ParentInventory == null) { disconnectedWire.Item.Drop(c.Character); - GameServer.Log(c.Character.LogName + " dropped " + disconnectedWire.Name, ServerLog.MessageType.Inventory); + GameServer.Log(GameServer.CharacterLogName(c.Character) + " dropped " + disconnectedWire.Name, ServerLog.MessageType.Inventory); } } @@ -177,13 +180,13 @@ namespace Barotrauma.Items.Components if (otherConnection == null) { - GameServer.Log(c.Character.LogName + " connected a wire to " + + GameServer.Log(GameServer.CharacterLogName(c.Character) + " connected a wire to " + Connections[i].Item.Name + " (" + Connections[i].Name + ")", ServerLog.MessageType.Wiring); } else { - GameServer.Log(c.Character.LogName + " connected a wire from " + + GameServer.Log(GameServer.CharacterLogName(c.Character) + " connected a wire from " + Connections[i].Item.Name + " (" + Connections[i].Name + ") to " + (otherConnection == null ? "none" : otherConnection.Item.Name + " (" + (otherConnection.Name) + ")"), ServerLog.MessageType.Wiring); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs index 7786d2621..58c896d2e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/CustomInterface.cs @@ -10,9 +10,17 @@ namespace Barotrauma.Items.Components public void ServerRead(ClientNetObject type, IReadMessage msg, Client c) { bool[] elementStates = new bool[customInterfaceElementList.Count]; + string[] elementValues = new string[customInterfaceElementList.Count]; for (int i = 0; i < customInterfaceElementList.Count; i++) { - elementStates[i] = msg.ReadBoolean(); + if (!string.IsNullOrEmpty(customInterfaceElementList[i].PropertyName)) + { + elementValues[i] = msg.ReadString(); + } + else + { + elementStates[i] = msg.ReadBoolean(); + } } CustomInterfaceElement clickedButton = null; @@ -20,7 +28,11 @@ namespace Barotrauma.Items.Components { for (int i = 0; i < customInterfaceElementList.Count; i++) { - if (customInterfaceElementList[i].ContinuousSignal) + if (!string.IsNullOrEmpty(customInterfaceElementList[i].PropertyName)) + { + TextChanged(customInterfaceElementList[i], elementValues[i]); + } + else if (customInterfaceElementList[i].ContinuousSignal) { TickBoxToggled(customInterfaceElementList[i], elementStates[i]); } @@ -48,7 +60,11 @@ namespace Barotrauma.Items.Components //extradata contains an array of buttons clicked by a client (or nothing if nothing was clicked) for (int i = 0; i < customInterfaceElementList.Count; i++) { - if (customInterfaceElementList[i].ContinuousSignal) + if (!string.IsNullOrEmpty(customInterfaceElementList[i].PropertyName)) + { + msg.Write(customInterfaceElementList[i].Signal); + } + else if(customInterfaceElementList[i].ContinuousSignal) { msg.Write(customInterfaceElementList[i].State); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs index 7bd88be3d..1950c4adf 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Terminal.cs @@ -14,7 +14,7 @@ namespace Barotrauma.Items.Components { newOutputValue = newOutputValue.Substring(0, MaxMessageLength); } - GameServer.Log(c.Character.LogName + " entered \"" + newOutputValue + "\" on " + item.Name, + GameServer.Log(GameServer.CharacterLogName(c.Character) + " entered \"" + newOutputValue + "\" on " + item.Name, ServerLog.MessageType.ItemInteraction); OutputValue = newOutputValue; item.SendSignal(0, newOutputValue, "signal_out", null); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Wire.cs index aa741edfe..7586f86c0 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/Signal/Wire.cs @@ -6,7 +6,7 @@ namespace Barotrauma.Items.Components { partial class Wire : ItemComponent, IDrawableComponent, IServerSerializable { - private void CreateNetworkEvent() + public void CreateNetworkEvent() { if (GameMain.Server == null) return; //split into multiple events because one might not be enough to fit all the nodes diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs index 907307bef..0574ec775 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs @@ -66,6 +66,19 @@ namespace Barotrauma Item droppedItem = Items[i]; Entity prevOwner = Owner; droppedItem.Drop(null); + + var previousInventory = prevOwner switch + { + Item itemInventory => (itemInventory.FindParentInventory(inventory => inventory is CharacterInventory) as CharacterInventory), + Character character => character.Inventory, + _ => null + }; + + if (previousInventory != null && previousInventory != c.Character?.Inventory) + { + GameMain.Server?.KarmaManager.OnItemTakenFromPlayer(previousInventory, c, droppedItem); + } + if (droppedItem.body != null && prevOwner != null) { droppedItem.body.SetTransform(prevOwner.SimPosition, 0.0f); @@ -88,6 +101,10 @@ namespace Barotrauma if (!prevItems.Contains(item) && !item.CanClientAccess(c)) { + if (item.body != null && !c.PendingPositionUpdates.Contains(item)) + { + c.PendingPositionUpdates.Enqueue(item); + } item.PositionUpdateInterval = 0.0f; continue; } @@ -106,7 +123,7 @@ namespace Barotrauma CreateNetworkEvent(); foreach (Inventory prevInventory in prevItemInventories.Distinct()) { - if (prevInventory != this) prevInventory?.CreateNetworkEvent(); + if (prevInventory != this) { prevInventory?.CreateNetworkEvent(); } } foreach (Item item in Items.Distinct()) @@ -116,11 +133,11 @@ namespace Barotrauma { if (Owner == c.Character) { - GameServer.Log(c.Character.LogName+ " picked up " + item.Name, ServerLog.MessageType.Inventory); + GameServer.Log(GameServer.CharacterLogName(c.Character) + " picked up " + item.Name, ServerLog.MessageType.Inventory); } else { - GameServer.Log(c.Character.LogName + " placed " + item.Name + " in " + Owner, ServerLog.MessageType.Inventory); + GameServer.Log(GameServer.CharacterLogName(c.Character) + " placed " + item.Name + " in " + Owner, ServerLog.MessageType.Inventory); } } } @@ -131,11 +148,11 @@ namespace Barotrauma { if (Owner == c.Character) { - GameServer.Log(c.Character.LogName + " dropped " + item.Name, ServerLog.MessageType.Inventory); + GameServer.Log(GameServer.CharacterLogName(c.Character) + " dropped " + item.Name, ServerLog.MessageType.Inventory); } else { - GameServer.Log(c.Character.LogName + " removed " + item.Name + " from " + Owner, ServerLog.MessageType.Inventory); + GameServer.Log(GameServer.CharacterLogName(c.Character) + " removed " + item.Name + " from " + Owner, ServerLog.MessageType.Inventory); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index 9545c41b6..db01d330e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -186,12 +186,12 @@ namespace Barotrauma if (ContainedItems == null || ContainedItems.All(i => i == null)) { - GameServer.Log(c.Character.LogName + " used item " + Name, ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(c.Character) + " used item " + Name, ServerLog.MessageType.ItemInteraction); } else { GameServer.Log( - c.Character.LogName + " used item " + Name + " (contained items: " + string.Join(", ", ContainedItems.Select(i => i.Name)) + ")", + GameServer.CharacterLogName(c.Character) + " used item " + Name + " (contained items: " + string.Join(", ", ContainedItems.Select(i => i.Name)) + ")", ServerLog.MessageType.ItemInteraction); } @@ -213,7 +213,7 @@ namespace Barotrauma } } - public void WriteSpawnData(IWriteMessage msg, UInt16 entityID) + public void WriteSpawnData(IWriteMessage msg, UInt16 entityID, UInt16 originalInventoryID) { if (GameMain.Server == null) return; @@ -227,7 +227,7 @@ namespace Barotrauma msg.Write(entityID); - if (ParentInventory == null || ParentInventory.Owner == null) + if (ParentInventory == null || ParentInventory.Owner == null || originalInventoryID == 0) { msg.Write((ushort)0); @@ -237,7 +237,7 @@ namespace Barotrauma } else { - msg.Write(ParentInventory.Owner.ID); + msg.Write(originalInventoryID); //find the index of the ItemContainer this item is inside to get the item to //spawn in the correct inventory in multi-inventory items like fabricators @@ -260,6 +260,8 @@ namespace Barotrauma msg.Write(slotIndex < 0 ? (byte)255 : (byte)slotIndex); } + msg.Write(body == null ? (byte)0 : (byte)body.BodyType); + byte teamID = 0; foreach (WifiComponent wifiComponent in GetComponents()) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs index 0924c5e72..216026bfc 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs @@ -347,7 +347,7 @@ namespace Barotrauma.Networking BannedPlayer bannedPlayer = bannedPlayers.Find(p => p.UniqueIdentifier == id); if (bannedPlayer != null) { - GameServer.Log(c.Name + " unbanned " + bannedPlayer.Name + " (" + bannedPlayer.IP + ")", ServerLog.MessageType.ConsoleUsage); + GameServer.Log(GameServer.ClientLogName(c) + " unbanned " + bannedPlayer.Name + " (" + bannedPlayer.IP + ")", ServerLog.MessageType.ConsoleUsage); RemoveBan(bannedPlayer); } } @@ -358,7 +358,7 @@ namespace Barotrauma.Networking BannedPlayer bannedPlayer = bannedPlayers.Find(p => p.UniqueIdentifier == id); if (bannedPlayer != null) { - GameServer.Log(c.Name + " rangebanned " + bannedPlayer.Name + " (" + bannedPlayer.IP + ")", ServerLog.MessageType.ConsoleUsage); + GameServer.Log(GameServer.ClientLogName(c) + " rangebanned " + bannedPlayer.Name + " (" + bannedPlayer.IP + ")", ServerLog.MessageType.ConsoleUsage); RangeBan(bannedPlayer); } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs index 84044b2af..e2468f5cc 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ChatMessage.cs @@ -159,6 +159,7 @@ namespace Barotrauma.Networking msg.Write((byte)ServerNetObject.CHAT_MESSAGE); msg.Write(NetStateID); msg.Write((byte)Type); + msg.Write((byte)ChangeType); msg.Write(Text); msg.Write(SenderName); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs index 0c60acc5b..7d30c1db1 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs @@ -75,7 +75,8 @@ namespace Barotrauma.Networking public bool SpectateOnly; public int KarmaKickCount; - + + private float syncedKarma = 100.0f; private float karma = 100.0f; public float Karma { @@ -89,6 +90,11 @@ namespace Barotrauma.Networking { if (GameMain.Server == null || !GameMain.Server.ServerSettings.KarmaEnabled) { return; } karma = Math.Min(Math.Max(value, 0.0f), 100.0f); + if (!MathUtils.NearlyEqual(karma, syncedKarma, 10.0f)) + { + syncedKarma = karma; + GameMain.NetworkMember.LastClientListUpdateID++; + } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/EntitySpawner.cs index ac4111dea..b43825dde 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/EntitySpawner.cs @@ -35,7 +35,7 @@ namespace Barotrauma { message.Write((byte)SpawnableType.Item); DebugConsole.Log("Writing item spawn data " + entities.Entity.ToString() + " (original ID: " + entities.OriginalID + ", current ID: " + entities.Entity.ID + ")"); - ((Item)entities.Entity).WriteSpawnData(message, entities.OriginalID); + ((Item)entities.Entity).WriteSpawnData(message, entities.OriginalID, entities.OriginalInventoryID); } else if (entities.Entity is Character) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs index d205980e9..74704c129 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/FileTransfer/FileSender.cs @@ -332,7 +332,7 @@ namespace Barotrauma.Networking case (byte)FileTransferType.Submarine: string fileName = inc.ReadString(); string fileHash = inc.ReadString(); - var requestedSubmarine = Submarine.SavedSubmarines.FirstOrDefault(s => s.Name == fileName && s.MD5Hash.Hash == fileHash); + var requestedSubmarine = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == fileName && s.MD5Hash.Hash == fileHash); if (requestedSubmarine != null) { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index cee416d35..d68545d1a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -205,12 +205,12 @@ namespace Barotrauma.Networking GameMain.NetLobbyScreen.RandomizeSettings(); if (!string.IsNullOrEmpty(serverSettings.SelectedSubmarine)) { - Submarine sub = Submarine.SavedSubmarines.FirstOrDefault(s => s.Name == serverSettings.SelectedSubmarine); + SubmarineInfo sub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == serverSettings.SelectedSubmarine); if (sub != null) { GameMain.NetLobbyScreen.SelectedSub = sub; } } if (!string.IsNullOrEmpty(serverSettings.SelectedShuttle)) { - Submarine shuttle = Submarine.SavedSubmarines.FirstOrDefault(s => s.Name == serverSettings.SelectedShuttle); + SubmarineInfo shuttle = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == serverSettings.SelectedShuttle); if (shuttle != null) { GameMain.NetLobbyScreen.SelectedShuttle = shuttle; } } started = true; @@ -279,7 +279,7 @@ namespace Barotrauma.Networking SendConsoleMessage("Granted all permissions to " + newClient.Name + ".", newClient); } - SendChatMessage($"ServerMessage.JoinedServer~[client]={clName}", ChatMessageType.Server, null); + SendChatMessage($"ServerMessage.JoinedServer~[client]={clName}", ChatMessageType.Server, null, changeType: PlayerConnectionChangeType.Joined); serverSettings.ServerDetailsChanged = true; if (previousPlayer != null && previousPlayer.Name != newClient.Name) @@ -338,6 +338,8 @@ namespace Barotrauma.Networking fileSender.Update(deltaTime); KarmaManager.UpdateClients(ConnectedClients, deltaTime); + UpdatePing(); + if (serverSettings.VoiceChatEnabled) { VoipServer.SendToClients(connectedClients); @@ -382,7 +384,7 @@ namespace Barotrauma.Networking } bool isCrewDead = - connectedClients.All(c => c.Character == null || c.Character.IsDead || c.Character.IsUnconscious); + connectedClients.All(c => c.Character == null || c.Character.IsDead || c.Character.IsIncapacitated); bool subAtLevelEnd = false; if (Submarine.MainSub != null && Submarine.MainSubs[1] == null) @@ -529,7 +531,7 @@ namespace Barotrauma.Networking c.ChatSpamSpeed = Math.Max(0.0f, c.ChatSpamSpeed - deltaTime); //constantly increase AFK timer if the client is controlling a character (gets reset to zero every time an input is received) - if (gameStarted && c.Character != null && !c.Character.IsDead && !c.Character.IsUnconscious) + if (gameStarted && c.Character != null && !c.Character.IsDead && !c.Character.IsIncapacitated) { if (c.Connection != OwnerConnection) c.KickAFKTimer += deltaTime; } @@ -611,6 +613,41 @@ namespace Barotrauma.Networking } } + + private double lastPingTime; + private byte[] lastPingData; + private void UpdatePing() + { + if (Timing.TotalTime > lastPingTime + 1.0) + { + lastPingData ??= new byte[64]; + for (int i=0;i + { + IWriteMessage pingReq = new WriteOnlyMessage(); + pingReq.Write((byte)ServerPacketHeader.PING_REQUEST); + pingReq.Write((byte)lastPingData.Length); + pingReq.Write(lastPingData, 0, lastPingData.Length); + serverPeer.Send(pingReq, c.Connection, DeliveryMethod.Unreliable); + + IWriteMessage pingInf = new WriteOnlyMessage(); + pingInf.Write((byte)ServerPacketHeader.CLIENT_PINGS); + pingInf.Write((byte)ConnectedClients.Count); + ConnectedClients.ForEach(c2 => + { + pingInf.Write(c2.ID); + pingInf.Write(c2.Ping); + }); + serverPeer.Send(pingInf, c.Connection, DeliveryMethod.Unreliable); + }); + } + } + private void ReadDataMessage(NetworkConnection sender, IReadMessage inc) { var connectedClient = connectedClients.Find(c => c.Connection == sender); @@ -618,6 +655,16 @@ namespace Barotrauma.Networking ClientPacketHeader header = (ClientPacketHeader)inc.ReadByte(); switch (header) { + case ClientPacketHeader.PING_RESPONSE: + byte responseLen = inc.ReadByte(); + if (responseLen != lastPingData.Length) { return; } + for (int i=0;i s.Name == subName && s.MD5Hash.Hash == subHash); + var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.Hash == subHash); if (matchingSub == null) { @@ -748,11 +795,11 @@ namespace Barotrauma.Networking if (Level.Loaded != null && levelEqualityCheckVal != Level.Loaded.EqualityCheckVal) { errorStr += " Level equality check failed. The level generated at your end doesn't match the level generated by the server(seed: " + Level.Loaded.Seed + - ", sub: " + Submarine.MainSub.Name + " (" + Submarine.MainSub.MD5Hash.ShortHash + ")" + + ", sub: " + Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash.ShortHash + ")" + ", mirrored: " + Level.Loaded.Mirrored + ")."; } - Log(c.Name + " has reported an error: " + errorStr, ServerLog.MessageType.Error); + Log(GameServer.ClientLogName(c) + " has reported an error: " + errorStr, ServerLog.MessageType.Error); GameAnalyticsManager.AddErrorEventOnce("GameServer.HandleClientError:" + errorStr, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorStr); try @@ -783,8 +830,8 @@ namespace Barotrauma.Networking Directory.CreateDirectory(ServerLog.SavePath); } - string filePath = "event_error_log_server_" + client.Name + "_" + ToolBox.RemoveInvalidFileNameChars(DateTime.UtcNow.ToShortTimeString() + ".log"); - filePath = Path.Combine(ServerLog.SavePath, filePath); + string filePath = "event_error_log_server_" + client.Name + "_" + DateTime.UtcNow.ToShortTimeString() + ".log"; + filePath = Path.Combine(ServerLog.SavePath, ToolBox.RemoveInvalidFileNameChars(filePath)); if (File.Exists(filePath)) { return; } List errorLines = new List @@ -798,7 +845,7 @@ namespace Barotrauma.Networking } if (GameMain.GameSession?.Submarine != null) { - errorLines.Add("Submarine: " + GameMain.GameSession.Submarine.Name); + errorLines.Add("Submarine: " + GameMain.GameSession.Submarine.Info.Name); } if (Level.Loaded != null) { @@ -1068,7 +1115,7 @@ namespace Barotrauma.Networking } else if (!sender.HasPermission(command)) { - Log("Client \"" + sender.Name + "\" sent a server command \"" + command + "\". Permission denied.", ServerLog.MessageType.ServerMessage); + Log("Client \"" + GameServer.ClientLogName(sender) + "\" sent a server command \"" + command + "\". Permission denied.", ServerLog.MessageType.ServerMessage); return; } @@ -1077,10 +1124,10 @@ namespace Barotrauma.Networking case ClientPermissions.Kick: string kickedName = inc.ReadString().ToLowerInvariant(); string kickReason = inc.ReadString(); - var kickedClient = connectedClients.Find(cl => cl != sender && cl.Name.ToLowerInvariant() == kickedName && cl.Connection != OwnerConnection); + var kickedClient = connectedClients.Find(cl => cl != sender && cl.Name.Equals(kickedName, StringComparison.OrdinalIgnoreCase) && cl.Connection != OwnerConnection); if (kickedClient != null) { - Log("Client \"" + sender.Name + "\" kicked \"" + kickedClient.Name + "\".", ServerLog.MessageType.ServerMessage); + Log("Client \"" + GameServer.ClientLogName(sender) + "\" kicked \"" + GameServer.ClientLogName(kickedClient) + "\".", ServerLog.MessageType.ServerMessage); KickClient(kickedClient, string.IsNullOrEmpty(kickReason) ? $"ServerMessage.KickedBy~[initiator]={sender.Name}" : kickReason); } else @@ -1094,10 +1141,10 @@ namespace Barotrauma.Networking bool range = inc.ReadBoolean(); double durationSeconds = inc.ReadDouble(); - var bannedClient = connectedClients.Find(cl => cl != sender && cl.Name.ToLowerInvariant() == bannedName && cl.Connection != OwnerConnection); + var bannedClient = connectedClients.Find(cl => cl != sender && cl.Name.Equals(bannedName, StringComparison.OrdinalIgnoreCase) && cl.Connection != OwnerConnection); if (bannedClient != null) { - Log("Client \"" + sender.Name + "\" banned \"" + bannedClient.Name + "\".", ServerLog.MessageType.ServerMessage); + Log("Client \"" + GameServer.ClientLogName(sender) + "\" banned \"" + GameServer.ClientLogName(bannedClient) + "\".", ServerLog.MessageType.ServerMessage); if (durationSeconds > 0) { BanClient(bannedClient, string.IsNullOrEmpty(banReason) ? $"ServerMessage.BannedBy~[initiator]={sender.Name}" : banReason, range, TimeSpan.FromSeconds(durationSeconds)); @@ -1121,12 +1168,12 @@ namespace Barotrauma.Networking bool end = inc.ReadBoolean(); if (gameStarted && end) { - Log("Client \"" + sender.Name + "\" ended the round.", ServerLog.MessageType.ServerMessage); + Log("Client \"" + GameServer.ClientLogName(sender) + "\" ended the round.", ServerLog.MessageType.ServerMessage); EndGame(); } else if (!gameStarted && !end && !initiatedStartGame) { - Log("Client \"" + sender.Name + "\" started the round.", ServerLog.MessageType.ServerMessage); + Log("Client \"" + GameServer.ClientLogName(sender) + "\" started the round.", ServerLog.MessageType.ServerMessage); StartGame(); } break; @@ -1137,7 +1184,7 @@ namespace Barotrauma.Networking var subList = GameMain.NetLobbyScreen.GetSubList(); if (subIndex >= subList.Count) { - DebugConsole.NewMessage("Client \"" + sender.Name + "\" attempted to select a sub, index out of bounds (" + subIndex + ")", Color.Red); + DebugConsole.NewMessage("Client \"" + GameServer.ClientLogName(sender) + "\" attempted to select a sub, index out of bounds (" + subIndex + ")", Color.Red); } else { @@ -1153,7 +1200,7 @@ namespace Barotrauma.Networking break; case ClientPermissions.SelectMode: UInt16 modeIndex = inc.ReadUInt16(); - if (GameMain.NetLobbyScreen.GameModes[modeIndex].Identifier.ToLowerInvariant() == "multiplayercampaign") + if (GameMain.NetLobbyScreen.GameModes[modeIndex].Identifier.Equals("multiplayercampaign", StringComparison.OrdinalIgnoreCase)) { string[] saveFiles = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Multiplayer).ToArray(); for (int i = 0; i < saveFiles.Length; i++) @@ -1220,12 +1267,12 @@ namespace Barotrauma.Networking string logMsg; if (permissionNames.Any()) { - logMsg = "Client \"" + sender.Name + "\" set the permissions of the client \"" + targetClient.Name + "\" to " + logMsg = "Client \"" + GameServer.ClientLogName(sender) + "\" set the permissions of the client \"" + GameServer.ClientLogName(targetClient) + "\" to " + string.Join(", ", permissionNames); } else { - logMsg = "Client \"" + sender.Name + "\" removed all permissions from the client \"" + targetClient.Name + "."; + logMsg = "Client \"" + GameServer.ClientLogName(sender) + "\" removed all permissions from the client \"" + GameServer.ClientLogName(targetClient) + "."; } Log(logMsg, ServerLog.MessageType.ServerMessage); @@ -1342,7 +1389,7 @@ namespace Barotrauma.Networking { //if docked to a sub with a smaller ID, don't send an update // (= update is only sent for the docked sub that has the smallest ID, doesn't matter if it's the main sub or a shuttle) - if (sub.IsOutpost || sub.DockedTo.Any(s => s.ID < sub.ID)) continue; + if (sub.Info.IsOutpost || sub.DockedTo.Any(s => s.ID < sub.ID)) continue; if (!c.PendingPositionUpdates.Contains(sub)) c.PendingPositionUpdates.Enqueue(sub); } @@ -1484,9 +1531,21 @@ namespace Barotrauma.Networking outmsg.Write(client.Name); outmsg.Write(client.Character == null || !gameStarted ? (client.PreferredJob ?? "") : ""); outmsg.Write(client.Character == null || !gameStarted ? (ushort)0 : client.Character.ID); + if (c.HasPermission(ClientPermissions.ServerLog)) + { + outmsg.Write(client.Karma); + } + else + { + outmsg.Write(100.0f); + } outmsg.Write(client.Muted); outmsg.Write(client.InGame); - outmsg.Write(client.Connection != OwnerConnection); //is kicking the player allowed + outmsg.Write(client.Permissions != ClientPermissions.None); + outmsg.Write(client.Connection != OwnerConnection && + !client.HasPermission(ClientPermissions.Ban) && + !client.HasPermission(ClientPermissions.Kick) && + !client.HasPermission(ClientPermissions.Unban)); //is kicking the player allowed outmsg.WritePadBits(); } } @@ -1655,12 +1714,12 @@ namespace Barotrauma.Networking Log("Starting a new round...", ServerLog.MessageType.ServerMessage); - Submarine selectedSub = null; - Submarine selectedShuttle = GameMain.NetLobbyScreen.SelectedShuttle; + SubmarineInfo selectedSub = null; + SubmarineInfo selectedShuttle = GameMain.NetLobbyScreen.SelectedShuttle; if (serverSettings.Voting.AllowSubVoting) { - selectedSub = serverSettings.Voting.HighestVoted(VoteType.Sub, connectedClients); + selectedSub = serverSettings.Voting.HighestVoted(VoteType.Sub, connectedClients); if (selectedSub == null) selectedSub = GameMain.NetLobbyScreen.SelectedSub; } else @@ -1692,7 +1751,7 @@ namespace Barotrauma.Networking return true; } - private IEnumerable InitiateStartGame(Submarine selectedSub, Submarine selectedShuttle, bool usingShuttle, GameModePreset selectedMode) + private IEnumerable InitiateStartGame(SubmarineInfo selectedSub, SubmarineInfo selectedShuttle, bool usingShuttle, GameModePreset selectedMode) { initiatedStartGame = true; @@ -1740,7 +1799,7 @@ namespace Barotrauma.Networking yield return CoroutineStatus.Success; } - private IEnumerable StartGame(Submarine selectedSub, Submarine selectedShuttle, bool usingShuttle, GameModePreset selectedMode) + private IEnumerable StartGame(SubmarineInfo selectedSub, SubmarineInfo selectedShuttle, bool usingShuttle, GameModePreset selectedMode) { entityEventManager.Clear(); @@ -1805,12 +1864,11 @@ namespace Barotrauma.Networking SendStartMessage(roundStartSeed, campaign.Map.SelectedConnection.Level.Seed, GameMain.GameSession, connectedClients, false); GameMain.GameSession.StartRound(campaign.Map.SelectedConnection.Level, - reloadSub: true, mirrorLevel: campaign.Map.CurrentLocation != campaign.Map.SelectedConnection.Locations[0]); campaign.AssignClientCharacterInfos(connectedClients); Log("Game mode: " + selectedMode.Name, ServerLog.MessageType.ServerMessage); - Log("Submarine: " + GameMain.GameSession.Submarine.Name, ServerLog.MessageType.ServerMessage); + Log("Submarine: " + GameMain.GameSession.SubmarineInfo.Name, ServerLog.MessageType.ServerMessage); Log("Level seed: " + campaign.Map.SelectedConnection.Level.Seed, ServerLog.MessageType.ServerMessage); } else @@ -1823,11 +1881,11 @@ namespace Barotrauma.Networking Log("Level seed: " + GameMain.NetLobbyScreen.LevelSeed, ServerLog.MessageType.ServerMessage); } - if (GameMain.GameSession.Submarine.IsFileCorrupted) + if (GameMain.GameSession.SubmarineInfo.IsFileCorrupted) { CoroutineManager.StopCoroutines(startGameCoroutine); initiatedStartGame = false; - SendChatMessage(TextManager.FormatServerMessage($"SubLoadError~[subname]={GameMain.GameSession.Submarine.Name}"), ChatMessageType.Error); + SendChatMessage(TextManager.FormatServerMessage($"SubLoadError~[subname]={GameMain.GameSession.SubmarineInfo.Name}"), ChatMessageType.Error); yield return CoroutineStatus.Failure; } @@ -1836,6 +1894,7 @@ namespace Barotrauma.Networking if (serverSettings.AllowRespawn && missionAllowRespawn) { respawnManager = new RespawnManager(this, usingShuttle ? selectedShuttle : null); } + Level.Loaded?.SpawnCorpses(); AutoItemPlacer.PlaceIfNeeded(GameMain.GameSession.GameMode); entityEventManager.RefreshEntityIDs(); @@ -1993,8 +2052,8 @@ namespace Barotrauma.Networking msg.Write((byte)GameMain.NetLobbyScreen.MissionType); - msg.Write(gameSession.Submarine.Name); - msg.Write(gameSession.Submarine.MD5Hash.Hash); + msg.Write(gameSession.SubmarineInfo.Name); + msg.Write(gameSession.SubmarineInfo.MD5Hash.Hash); msg.Write(serverSettings.UseRespawnShuttle); msg.Write(GameMain.NetLobbyScreen.SelectedShuttle.Name); msg.Write(GameMain.NetLobbyScreen.SelectedShuttle.MD5Hash.Hash); @@ -2139,7 +2198,16 @@ namespace Barotrauma.Networking public override void AddChatMessage(ChatMessage message) { if (string.IsNullOrEmpty(message.Text)) { return; } - Log(message.TextWithSender, ServerLog.MessageType.Chat); + string logMsg; + if (message.SenderClient != null) + { + logMsg = GameServer.ClientLogName(message.SenderClient) + ": " + message.TranslatedText; + } + else + { + logMsg = message.TextWithSender; + } + Log(logMsg, ServerLog.MessageType.Chat); base.AddChatMessage(message); } @@ -2192,11 +2260,9 @@ namespace Barotrauma.Networking public override void KickPlayer(string playerName, string reason) { - playerName = playerName.ToLowerInvariant(); - Client client = connectedClients.Find(c => - c.Name.ToLowerInvariant() == playerName || - (c.Character != null && c.Character.Name.ToLowerInvariant() == playerName)); + c.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase) || + (c.Character != null && c.Character.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase))); KickClient(client, reason); } @@ -2225,16 +2291,14 @@ namespace Barotrauma.Networking string msg = DisconnectReason.Kicked.ToString(); string logMsg = $"ServerMessage.KickedFromServer~[client]={client.Name}"; - DisconnectClient(client, logMsg, msg, reason); + DisconnectClient(client, logMsg, msg, reason, PlayerConnectionChangeType.Kicked); } public override void BanPlayer(string playerName, string reason, bool range = false, TimeSpan? duration = null) { - playerName = playerName.ToLowerInvariant(); - Client client = connectedClients.Find(c => - c.Name.ToLowerInvariant() == playerName || - (c.Character != null && c.Character.Name.ToLowerInvariant() == playerName)); + c.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase) || + (c.Character != null && c.Character.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase))); if (client == null) { @@ -2258,7 +2322,7 @@ namespace Barotrauma.Networking client.Karma = Math.Max(client.Karma, 50.0f); string targetMsg = DisconnectReason.Banned.ToString(); - DisconnectClient(client, $"ServerMessage.BannedFromServer~[client]={client.Name}", targetMsg, reason); + DisconnectClient(client, $"ServerMessage.BannedFromServer~[client]={client.Name}", targetMsg, reason, PlayerConnectionChangeType.Banned); if (client.SteamID == 0 || range) { @@ -2302,10 +2366,10 @@ namespace Barotrauma.Networking Client client = connectedClients.Find(x => x.Connection == senderConnection); if (client == null) return; - DisconnectClient(client, msg, targetmsg, string.Empty); + DisconnectClient(client, msg, targetmsg, string.Empty, PlayerConnectionChangeType.Disconnected); } - public void DisconnectClient(Client client, string msg = "", string targetmsg = "", string reason = "") + public void DisconnectClient(Client client, string msg = "", string targetmsg = "", string reason = "", PlayerConnectionChangeType changeType = PlayerConnectionChangeType.Disconnected) { if (client == null) return; @@ -2352,7 +2416,7 @@ namespace Barotrauma.Networking UpdateVoteStatus(); - SendChatMessage(msg, ChatMessageType.Server); + SendChatMessage(msg, ChatMessageType.Server, changeType: changeType); UpdateCrewFrame(); @@ -2401,7 +2465,7 @@ namespace Barotrauma.Networking /// /// Add the message to the chatbox and pass it to all clients who can receive it /// - public void SendChatMessage(string message, ChatMessageType? type = null, Client senderClient = null, Character senderCharacter = null) + public void SendChatMessage(string message, ChatMessageType? type = null, Client senderClient = null, Character senderCharacter = null, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None) { string senderName = ""; @@ -2486,17 +2550,20 @@ namespace Barotrauma.Networking senderCharacter = senderClient.Character; senderName = senderCharacter == null ? senderClient.Name : senderCharacter.Name; + if (type == ChatMessageType.Private) + { + if (senderCharacter != null && !senderCharacter.IsDead || targetClient.Character != null && !targetClient.Character.IsDead) + { + //sender or target has an alive character, sending private messages not allowed + SendDirectChatMessage(ChatMessage.Create("", $"ServerMessage.PrivateMessagesNotAllowed", ChatMessageType.Error, null), senderClient); + return; + } + } //sender doesn't have a character or the character can't speak -> only ChatMessageType.Dead allowed - if (senderCharacter == null || senderCharacter.IsDead || senderCharacter.SpeechImpediment >= 100.0f) + else if (senderCharacter == null || senderCharacter.IsDead || senderCharacter.SpeechImpediment >= 100.0f) { type = ChatMessageType.Dead; } - else if (type == ChatMessageType.Private) - { - //sender has an alive character, sending private messages not allowed - return; - } - } } else @@ -2592,7 +2659,9 @@ namespace Barotrauma.Networking senderName, modifiedMessage, (ChatMessageType)type, - senderCharacter); + senderCharacter, + senderClient, + changeType); SendDirectChatMessage(chatMsg, client); } @@ -2663,6 +2732,9 @@ namespace Barotrauma.Networking var clientsToKick = connectedClients.FindAll(c => c.Connection != OwnerConnection && + !c.HasPermission(ClientPermissions.Kick) && + !c.HasPermission(ClientPermissions.Ban) && + !c.HasPermission(ClientPermissions.Unban) && c.KickVoteCount >= connectedClients.Count * serverSettings.KickVoteRequiredRatio); foreach (Client c in clientsToKick) { @@ -2673,7 +2745,7 @@ namespace Barotrauma.Networking previousPlayer.KickVoters.Clear(); } - SendChatMessage($"ServerMessage.KickedFromServer~[client]={c.Name}", ChatMessageType.Server, null); + SendChatMessage($"ServerMessage.KickedFromServer~[client]={c.Name}", ChatMessageType.Server, null, changeType: PlayerConnectionChangeType.Kicked); KickClient(c, "ServerMessage.KickedByVote"); BanClient(c, "ServerMessage.KickedByVoteAutoBan", duration: TimeSpan.FromSeconds(serverSettings.AutoBanTime)); } @@ -2857,6 +2929,11 @@ namespace Barotrauma.Networking { newCharacter.LastNetworkUpdateID = client.Character.LastNetworkUpdateID; } + + if (newCharacter.Info != null && newCharacter.Info.Character == null) + { + newCharacter.Info.Character = newCharacter; + } newCharacter.OwnerClientEndPoint = client.Connection.EndPointString; newCharacter.OwnerClientName = client.Name; @@ -3203,6 +3280,25 @@ namespace Barotrauma.Networking } } + public static string ClientLogName(Client client, string name = null) + { + if (client == null) { return name; } + string retVal = "‖"; + if (client.Karma < 40.0f) + { + retVal += "color:#ff9900;"; + } + retVal += "metadata:" + (client.SteamID!=0 ? client.SteamID.ToString() : client.ID.ToString()) + "‖" + (name ?? client.Name) + "‖end‖"; + return retVal; + } + + public static string CharacterLogName(Character character) + { + if (character == null) { return "[NULL]"; } + Client client = GameMain.Server.ConnectedClients.Find(c => c.Character == character); + return ClientLogName(client, character.LogName); + } + public static void Log(string line, ServerLog.MessageType messageType) { if (GameMain.Server == null || !GameMain.Server.ServerSettings.SaveServerLogs) return; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs index 8b5227fdb..1ecbbab59 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/KarmaManager.cs @@ -3,6 +3,7 @@ using Barotrauma.Networking; using System; using System.Collections.Generic; using System.Linq; +using Barotrauma.Extensions; namespace Barotrauma { @@ -12,8 +13,18 @@ namespace Barotrauma { public List> WireDisconnectTime = new List>(); + public struct TimeAmount + { + public double Time; + public float Amount; + } + + public List KarmaDecreasesInPastMinute = new List(); + public float PreviousNotifiedKarma; + public double PreviousKarmaNotificationTime; + public float StructureDamageAccumulator; private float structureDamagePerSecond; @@ -23,6 +34,9 @@ namespace Barotrauma set { structureDamagePerSecond = value; } } + public List StunsInPastMinute = new List(); + public float StunKarmaDecreaseMultiplier; + //when did a given character last attack this one public Dictionary LastAttackTime { @@ -53,6 +67,13 @@ namespace Barotrauma clientMemory.StructureDamagePerSecond = clientMemory.StructureDamageAccumulator; clientMemory.StructureDamageAccumulator = 0.0f; + clientMemory.StunsInPastMinute.RemoveAll(s => s.Time + 60.0f < Timing.TotalTime); + + if (!clientMemory.StunsInPastMinute.Any()) + { + clientMemory.StunKarmaDecreaseMultiplier = 1.0f; + } + var toRemove = clientMemory.LastAttackTime.Where(pair => pair.Value < Timing.TotalTime - AllowedRetaliationTime).Select(pair => pair.Key).ToList(); foreach (var lastAttacker in toRemove) { @@ -89,28 +110,26 @@ namespace Barotrauma var clientMemory = GetClientMemory(client); float karmaChange = client.Karma - clientMemory.PreviousNotifiedKarma; - if (Math.Abs(karmaChange) > 1.0f && - (TestMode || Math.Abs(karmaChange) / clientMemory.PreviousNotifiedKarma > KarmaNotificationInterval / 100.0f)) + if (Math.Abs(karmaChange) > 1.0f && TestMode) { - if (TestMode) + string msg = + karmaChange < 0 ? $"Your karma has decreased to {client.Karma}" : $"Your karma has increased to {client.Karma}"; + if (!string.IsNullOrEmpty(debugKarmaChangeReason)) { - string msg = - karmaChange < 0 ? $"Your karma has decreased to {client.Karma}" : $"Your karma has increased to {client.Karma}"; - if (!string.IsNullOrEmpty(debugKarmaChangeReason)) - { - msg += $". Reason: {debugKarmaChangeReason}"; - } - GameMain.Server.SendDirectChatMessage(msg, client); + msg += $". Reason: {debugKarmaChangeReason}"; } - else if (Math.Abs(KickBanThreshold - client.Karma) < KarmaNotificationInterval) - { - GameMain.Server.SendDirectChatMessage(TextManager.Get("KarmaBanWarning"), client); - } - else - { - GameMain.Server.SendDirectChatMessage(TextManager.Get(karmaChange < 0 ? "KarmaDecreasedUnknownAmount" : "KarmaIncreasedUnknownAmount"), client); - } - clientMemory.PreviousNotifiedKarma = client.Karma; + GameMain.Server.SendDirectChatMessage(msg, client); + clientMemory.PreviousNotifiedKarma = client.Karma; + clientMemory.PreviousKarmaNotificationTime = Timing.TotalTime; + } + else if (Timing.TotalTime >= clientMemory.PreviousKarmaNotificationTime + 5.0f && + clientMemory.PreviousNotifiedKarma >= KickBanThreshold + KarmaNotificationInterval && + client.Karma < KickBanThreshold + KarmaNotificationInterval) + { + GameMain.Server.SendDirectChatMessage(TextManager.Get("KarmaBanWarning"), client); + GameServer.Log(GameServer.ClientLogName(client) + " has been warned for having dangerously low karma.", ServerLog.MessageType.Karma); + clientMemory.PreviousNotifiedKarma = client.Karma; + clientMemory.PreviousKarmaNotificationTime = Timing.TotalTime; } } @@ -130,10 +149,10 @@ namespace Barotrauma //increase the strength of the herpes affliction in steps instead of linearly //otherwise clients could determine their exact karma value from the strength float herpesStrength = 0.0f; - if (client.Karma < 20) - herpesStrength = 100.0f; - else if (client.Karma < 30) - herpesStrength = 60.0f; + if (client.Karma < 20) + herpesStrength = 100.0f; + else if (client.Karma < 30) + herpesStrength = 60.0f; else if (client.Karma < 40.0f) herpesStrength = 30.0f; @@ -141,6 +160,8 @@ namespace Barotrauma if (existingAffliction == null && herpesStrength > 0.0f) { client.Character.CharacterHealth.ApplyAffliction(null, new Affliction(herpesAffliction, herpesStrength)); + GameServer.Log($"{GameServer.ClientLogName(client)} has contracted space herpes due to low karma.", ServerLog.MessageType.Karma); + GameMain.NetworkMember.LastClientListUpdateID++; } else if (existingAffliction != null) { @@ -205,7 +226,84 @@ namespace Barotrauma clientMemories.Remove(client); } - public void OnCharacterHealthChanged(Character target, Character attacker, float damage, IEnumerable appliedAfflictions = null) + // ReSharper disable once UseNegatedPatternMatching, LoopCanBeConvertedToQuery + public void OnItemTakenFromPlayer(CharacterInventory inventory, Client yoinker, Item item) + { + Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == inventory.Owner); + + Character yoinkerCharacter = yoinker?.Character; + Character targetCharacter = inventory.Owner as Character; + + if (yoinker == null || item == null || yoinkerCharacter == null || targetCharacter == null || yoinkerCharacter == targetCharacter) { return; } + + if (targetClient == null && (!DangerousItemStealBots || targetCharacter.AIController == null)) { return; } + + // Only if the target is alive and they are stunned, unconscious or handcuffed + if (targetCharacter.IsDead || targetCharacter.Removed || !(targetCharacter.Stun > 0) && !targetCharacter.IsUnconscious && !targetCharacter.LockHands) { return; } + + if (GameMain.Server.TraitorManager?.Traitors != null) + { + if (GameMain.Server.TraitorManager.Traitors.Any(t => t.Character == targetCharacter || t.Character == yoinkerCharacter)) + { + // Don't penalize traitors + return; + } + } + + var foundItem = Inventory.FindItemRecursive(item, it => it.Prefab.Identifier == "idcard" || it.GetComponent() != null || it.GetComponent() != null); + + if (foundItem == null) { return; } + + bool isIdCard = foundItem.prefab.Identifier == "idcard"; + bool isWeapon = foundItem.GetComponent() != null || foundItem.GetComponent() != null; + + if (isIdCard) + { + string name = string.Empty; + + foreach (var tag in foundItem.Tags.Split(',')) + { + string[] split = tag.Split(':'); + string key = split.Length > 0 ? split[0] : string.Empty; + string value = split.Length > 1 ? split[1] : string.Empty; + if (key == "name") { name = value; } + } + + // Name tag doesn't belong to anyone in particular or we own the ID card + if (name == null || name == yoinkerCharacter.Name) { return; } + } + + if (MathUtils.NearlyEqual(DangerousItemStealKarmaDecrease, 0)) { return; } + + const float calcUpper = 1, calcLower = -1; + + float upper = DangerousItemStealKarmaDecrease + 10.0f; + float lower = DangerousItemStealKarmaDecrease - 10.0f; + + if (lower < 0) + { + upper += Math.Abs(lower); + lower = 0; + } + + // If we're stealing from a bot assume the bot has 50 karma + var targetKarma = targetClient?.Karma ?? 50; + + float karmaDifference = Math.Clamp((targetKarma - yoinker.Karma) / 50.0f, calcLower, calcUpper); + float karmaDecrease = lower + (karmaDifference - calcLower) * (upper - lower) / (calcUpper - calcLower); + + JobPrefab clientJob = yoinker.CharacterInfo?.Job?.Prefab; + + // security officers receive less karma penalty + if (clientJob != null && clientJob.Identifier == "securityofficer" && isWeapon) + { + karmaDecrease *= 0.5f; + } + + AdjustKarma(yoinkerCharacter, -karmaDecrease, "Stolen dangerous item"); + } + + public void OnCharacterHealthChanged(Character target, Character attacker, float damage, float stun, IEnumerable appliedAfflictions = null) { if (target == null || attacker == null) { return; } if (target == attacker) { return; } @@ -238,11 +336,11 @@ namespace Barotrauma if (appliedAfflictions != null) { - foreach (Affliction affliction in appliedAfflictions) - { - if (MathUtils.NearlyEqual(affliction.Prefab.KarmaChangeOnApplied, 0.0f)) { continue; } - damage -= affliction.Prefab.KarmaChangeOnApplied * affliction.Strength; - } + foreach (Affliction affliction in appliedAfflictions) + { + if (MathUtils.NearlyEqual(affliction.Prefab.KarmaChangeOnApplied, 0.0f)) { continue; } + damage -= affliction.Prefab.KarmaChangeOnApplied * affliction.Strength; + } } Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == target); @@ -253,15 +351,16 @@ namespace Barotrauma } Client attackerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == attacker); - if (attackerClient != null) + ClientMemory attackerMemory = GetClientMemory(attackerClient); + if (attackerMemory != null) { //if the attacker has been attacked by the target within the last x seconds, ignore the damage //(= no karma penalty from retaliating against someone who attacked you) - var attackerMemory = GetClientMemory(attackerClient); if (attackerMemory.LastAttackTime.ContainsKey(target) && attackerMemory.LastAttackTime[target] > Timing.TotalTime - AllowedRetaliationTime) { damage = Math.Min(damage, 0); + stun = 0.0f; } } @@ -270,6 +369,7 @@ namespace Barotrauma target.HasEquippedItem("clowncostume")) { damage *= 0.5f; + stun *= 0.5f; } //smaller karma penalty for attacking someone who's aiming with a weapon @@ -278,6 +378,7 @@ namespace Barotrauma target.SelectedItems.Any(it => it != null && (it.GetComponent() != null || it.GetComponent() != null))) { damage *= 0.5f; + stun *= 0.5f; } //damage scales according to the karma of the target @@ -298,6 +399,30 @@ namespace Barotrauma } else { + if (stun > 0 && attackerMemory != null) + { + //GameServer.Log(GameServer.CharacterLogName(attacker) + " stunned " + GameServer.CharacterLogName(target) + $" ({stun})", ServerLog.MessageType.Karma); + attackerMemory.StunsInPastMinute.Add(new ClientMemory.TimeAmount() { Time = Timing.TotalTime, Amount = stun }); + + if (attackerMemory.StunsInPastMinute.Count > 1) + { + float avgStunsInflicted = attackerMemory.StunsInPastMinute[0].Amount / (float)(attackerMemory.StunsInPastMinute[1].Time - attackerMemory.StunsInPastMinute[0].Time); + for (int i = 1; i < attackerMemory.StunsInPastMinute.Count; i++) + { + avgStunsInflicted += attackerMemory.StunsInPastMinute[i].Amount / (float)(attackerMemory.StunsInPastMinute[i].Time - attackerMemory.StunsInPastMinute[i - 1].Time); + } + + //GameServer.Log(avgStunsInflicted.ToString(), ServerLog.MessageType.Karma); + + if (avgStunsInflicted > StunFriendlyKarmaDecreaseThreshold || + attackerMemory.StunKarmaDecreaseMultiplier > 1.0f) + { + AdjustKarma(attacker, -StunFriendlyKarmaDecrease * attackerMemory.StunKarmaDecreaseMultiplier, "Stunned friendly"); + attackerMemory.StunKarmaDecreaseMultiplier *= 2.0f; + } + } + } + if (damage > 0) { AdjustKarma(attacker, -damage * DamageFriendlyKarmaDecrease, "Damaged friendly"); @@ -395,6 +520,7 @@ namespace Barotrauma private ClientMemory GetClientMemory(Client client) { + if (client == null) { return null; } if (!clientMemories.ContainsKey(client)) { clientMemories[client] = new ClientMemory() @@ -429,6 +555,21 @@ namespace Barotrauma } client.Karma += amount; + + if (amount < 0.0f) + { + float? herpesStrength = client.Character?.CharacterHealth.GetAfflictionStrength("spaceherpes"); + var clientMemory = GetClientMemory(client); + clientMemory.KarmaDecreasesInPastMinute.RemoveAll(ta => ta.Time + 60.0f < Timing.TotalTime); + float aggregate = clientMemory.KarmaDecreasesInPastMinute.Select(ta => ta.Amount).DefaultIfEmpty().Aggregate((a, b) => a + b); + clientMemory.KarmaDecreasesInPastMinute.Add(new ClientMemory.TimeAmount() { Time = Timing.TotalTime, Amount = -amount }); + + if (herpesStrength.HasValue && herpesStrength <= 0.0f && aggregate - amount > 25.0f && aggregate <= 25.0f) + { + GameServer.Log($"{GameServer.ClientLogName(client)} has lost more than 25 karma in the past minute.", ServerLog.MessageType.Karma); + } + } + if (TestMode) { SendKarmaNotifications(client, debugKarmaChangeReason); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs index 12cd2c5e9..7dfb9041a 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs @@ -138,7 +138,12 @@ namespace Barotrauma.Networking //remove old events that have been sent to all clients, they are redundant now // keep at least one event in the list (lastSentToAll == e.ID) so we can use it to keep track of the latest ID // and events less than 15 seconds old to give disconnected clients a bit of time to reconnect without getting desynced - events.RemoveAll(e => (NetIdUtils.IdMoreRecent(lastSentToAll, e.ID) || !inGameClientsPresent) && e.CreateTime < Timing.TotalTime - 15.0f); + if (Timing.TotalTime > GameMain.GameSession.RoundStartTime + NetConfig.RoundStartSyncDuration) + { + events.RemoveAll(e => + (NetIdUtils.IdMoreRecent(lastSentToAll, e.ID) || !inGameClientsPresent) && + e.CreateTime < Timing.TotalTime - NetConfig.EventRemovalTime); + } for (int i = events.Count - 1; i >= 0; i--) { @@ -175,7 +180,7 @@ namespace Barotrauma.Networking //UNLESS the character is unconscious, in which case we'll read the messages immediately (because further inputs will be ignored) //atm the "give in" command is the only thing unconscious characters can do, other types of events are ignored - if (!bufferedEvent.Character.IsUnconscious && + if (!bufferedEvent.Character.IsIncapacitated && NetIdUtils.IdMoreRecent(bufferedEvent.CharacterStateID, bufferedEvent.Character.LastProcessedID)) { continue; @@ -224,7 +229,9 @@ namespace Barotrauma.Networking }); lastSentToAnyoneTime = events.Find(e => e.ID == lastSentToAnyone)?.CreateTime ?? Timing.TotalTime; - if ((Timing.TotalTime - lastSentToAnyoneTime) > 10.0 && (Timing.TotalTime - lastWarningTime) > 5.0) + if (Timing.TotalTime - lastWarningTime > 5.0 && + Timing.TotalTime - lastSentToAnyoneTime > 10.0 && + Timing.TotalTime > GameMain.GameSession.RoundStartTime + NetConfig.RoundStartSyncDuration) { lastWarningTime = Timing.TotalTime; GameServer.Log("WARNING: ServerEntityEventManager is lagging behind! Last sent id: " + lastSentToAnyone.ToString() + ", latest create id: " + ID.ToString(), ServerLog.MessageType.ServerMessage); @@ -235,19 +242,21 @@ namespace Barotrauma.Networking clients.Where(c => c.NeedsMidRoundSync).ForEach(c => { if (NetIdUtils.IdMoreRecent(lastSentToAll, c.FirstNewEventID)) lastSentToAll = (ushort)(c.FirstNewEventID - 1); }); ServerEntityEvent firstEventToResend = events.Find(e => e.ID == (ushort)(lastSentToAll + 1)); - if (firstEventToResend != null && ((lastSentToAnyoneTime - firstEventToResend.CreateTime) > 10.0 || (Timing.TotalTime - firstEventToResend.CreateTime) > 30.0)) + if (firstEventToResend != null && + Timing.TotalTime > GameMain.GameSession.RoundStartTime + NetConfig.RoundStartSyncDuration && + ((lastSentToAnyoneTime - firstEventToResend.CreateTime) > NetConfig.OldReceivedEventKickTime || (Timing.TotalTime - firstEventToResend.CreateTime) > NetConfig.OldEventKickTime)) { // This event is 10 seconds older than the last one we've successfully sent, // kick everyone that hasn't received it yet, this is way too old // UNLESS the event was created when the client was still midround syncing, // in which case we'll wait until the timeout runs out before kicking the client List toKick = inGameClients.FindAll(c => - NetIdUtils.IdMoreRecent((UInt16)(lastSentToAll + 1), c.LastRecvEntityEventID) && + NetIdUtils.IdMoreRecent((UInt16)(lastSentToAll + 1), c.LastRecvEntityEventID) && (firstEventToResend.CreateTime > c.MidRoundSyncTimeOut || lastSentToAnyoneTime > c.MidRoundSyncTimeOut || Timing.TotalTime > c.MidRoundSyncTimeOut + 10.0)); toKick.ForEach(c => { DebugConsole.NewMessage(c.Name + " was kicked due to excessive desync (expected old event " + (c.LastRecvEntityEventID + 1).ToString() + ")", Color.Red); - GameServer.Log("Disconnecting client " + c.Name + " due to excessive desync (expected old event " + GameServer.Log("Disconnecting client " + GameServer.ClientLogName(c) + " due to excessive desync (expected old event " + (c.LastRecvEntityEventID + 1).ToString() + " (created " + (Timing.TotalTime - firstEventToResend.CreateTime).ToString("0.##") + " s ago, " + (lastSentToAnyoneTime - firstEventToResend.CreateTime).ToString("0.##") + " s older than last event sent to anyone)" + @@ -265,7 +274,7 @@ namespace Barotrauma.Networking toKick.ForEach(c => { DebugConsole.NewMessage(c.Name + " was kicked due to excessive desync (expected removed event " + (c.LastRecvEntityEventID + 1).ToString() + ", last available is " + events[0].ID.ToString() + ")", Color.Red); - GameServer.Log("Disconnecting client " + c.Name + " due to excessive desync (expected removed event " + (c.LastRecvEntityEventID + 1).ToString() + ", last available is " + events[0].ID.ToString() + ")", ServerLog.MessageType.Error); + GameServer.Log("Disconnecting client " + GameServer.ClientLogName(c) + " due to excessive desync (expected removed event " + (c.LastRecvEntityEventID + 1).ToString() + ", last available is " + events[0].ID.ToString() + ")", ServerLog.MessageType.Error); server.DisconnectClient(c, "", DisconnectReason.ExcessiveDesyncRemovedEvent + "/ServerMessage.ExcessiveDesyncRemovedEvent"); }); } @@ -274,7 +283,7 @@ namespace Barotrauma.Networking var timedOutClients = clients.FindAll(c => c.Connection != GameMain.Server.OwnerConnection && c.InGame && c.NeedsMidRoundSync && Timing.TotalTime > c.MidRoundSyncTimeOut); foreach (Client timedOutClient in timedOutClients) { - GameServer.Log("Disconnecting client " + timedOutClient.Name + ". Syncing the client with the server took too long.", ServerLog.MessageType.Error); + GameServer.Log("Disconnecting client " + GameServer.ClientLogName(timedOutClient) + ". Syncing the client with the server took too long.", ServerLog.MessageType.Error); GameMain.Server.DisconnectClient(timedOutClient, "", DisconnectReason.SyncTimeout + "/ServerMessage.SyncTimeout"); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs index f64f8c05a..c8b48a8dd 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/LidgrenServerPeer.cs @@ -419,12 +419,12 @@ namespace Barotrauma.Networking //steam auth cannot be done (SteamManager not initialized or no ticket given), //but it's not required either -> let the client join without auth - if ((!Steam.SteamManager.IsInitialized || (ticket?.Length??0) == 0) && + if ((!Steam.SteamManager.IsInitialized || (ticket?.Length ?? 0) == 0) && !requireSteamAuth) { pendingClient.Name = name; pendingClient.OwnerKey = ownKey; - pendingClient.InitializationStep = ConnectionInitialization.ContentPackageOrder; + pendingClient.InitializationStep = serverSettings.HasPassword ? ConnectionInitialization.Password : ConnectionInitialization.ContentPackageOrder; } else { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs index d1a158d2e..f86579619 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/RespawnManager.cs @@ -258,6 +258,9 @@ namespace Barotrauma.Networking { bool bot = i >= clients.Count; + characterInfos[i].CurrentOrder = null; + characterInfos[i].CurrentOrderOption = null; + var character = Character.Create(characterInfos[i], shuttleSpawnPoints[i].WorldPosition, characterInfos[i].Name, !bot, bot); character.TeamID = Character.TeamType.Team1; @@ -279,7 +282,7 @@ namespace Barotrauma.Networking clients[i].Character = character; character.OwnerClientEndPoint = clients[i].Connection.EndPointString; character.OwnerClientName = clients[i].Name; - GameServer.Log(string.Format("Respawning {0} ({1}) as {2}", clients[i].Name, clients[i].Connection?.EndPointString, characterInfos[i].Job.Name), ServerLog.MessageType.Spawning); + GameServer.Log(string.Format("Respawning {0} ({1}) as {2}", GameServer.ClientLogName(clients[i]), clients[i].Connection?.EndPointString, characterInfos[i].Job.Name), ServerLog.MessageType.Spawning); } if (divingSuitPrefab != null && oxyPrefab != null && RespawnShuttle != null) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs index 77b9c4c1e..b2997a2f1 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/ServerSettings.cs @@ -108,7 +108,7 @@ namespace Barotrauma.Networking netProperties[key].Read(incMsg); if (!netProperties[key].PropEquals(prevValue, netProperties[key])) { - GameServer.Log(c.Name + " changed " + netProperties[key].Name + " to " + netProperties[key].Value.ToString(), ServerLog.MessageType.ServerMessage); + GameServer.Log(GameServer.ClientLogName(c) + " changed " + netProperties[key].Name + " to " + netProperties[key].Value.ToString(), ServerLog.MessageType.ServerMessage); } changed = true; } @@ -367,7 +367,7 @@ namespace Barotrauma.Networking if (clientElement.Attribute("preset") == null) { string permissionsStr = clientElement.GetAttributeString("permissions", ""); - if (permissionsStr.ToLowerInvariant() == "all") + if (permissionsStr.Equals("all", StringComparison.OrdinalIgnoreCase)) { foreach (ClientPermissions permission in Enum.GetValues(typeof(ClientPermissions))) { @@ -384,7 +384,7 @@ namespace Barotrauma.Networking { foreach (XElement commandElement in clientElement.Elements()) { - if (commandElement.Name.ToString().ToLowerInvariant() != "command") continue; + if (!commandElement.Name.ToString().Equals("command", StringComparison.OrdinalIgnoreCase)) { continue; } string commandName = commandElement.GetAttributeString("name", ""); DebugConsole.Command command = DebugConsole.FindCommand(commandName); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs index b923dc1d5..4fcdc6f73 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voip/VoipServer.cs @@ -89,7 +89,8 @@ namespace Barotrauma.Networking if (sender.Character != null && sender.Character.SpeechImpediment >= 100.0f) { return false; } //check if the message can be sent via radio - if (ChatMessage.CanUseRadio(sender.Character, out WifiComponent senderRadio) && + if (!sender.VoipQueue.ForceLocal && + ChatMessage.CanUseRadio(sender.Character, out WifiComponent senderRadio) && ChatMessage.CanUseRadio(recipient.Character, out WifiComponent recipientRadio)) { if (recipientRadio.CanReceive(senderRadio)) { return true; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs index 5a5962955..b04f53d5d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Voting.cs @@ -38,7 +38,7 @@ namespace Barotrauma { case VoteType.Sub: string subName = inc.ReadString(); - Submarine sub = Submarine.SavedSubmarines.FirstOrDefault(s => s.Name == subName); + SubmarineInfo sub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName); sender.SetVote(voteType, sub); break; @@ -74,7 +74,7 @@ namespace Barotrauma if (ready != sender.GetVote(VoteType.StartRound)) { sender.SetVote(VoteType.StartRound, ready); - GameServer.Log(sender.Name + (ready ? " is ready to start the game." : " is not ready to start the game."), ServerLog.MessageType.ServerMessage); + GameServer.Log(GameServer.ClientLogName(sender) + (ready ? " is ready to start the game." : " is not ready to start the game."), ServerLog.MessageType.ServerMessage); } break; @@ -97,7 +97,7 @@ namespace Barotrauma foreach (Pair vote in voteList) { msg.Write((byte)vote.Second); - msg.Write(((Submarine)vote.First).Name); + msg.Write(((SubmarineInfo)vote.First).Name); } } msg.Write(AllowModeVoting); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/WhiteList.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/WhiteList.cs index c50de1630..34d605887 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/WhiteList.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/WhiteList.cs @@ -181,7 +181,7 @@ namespace Barotrauma.Networking WhiteListedPlayer whitelistedPlayer = whitelistedPlayers.Find(p => p.UniqueIdentifier == id); if (whitelistedPlayer != null) { - GameServer.Log(c.Name + " removed " + whitelistedPlayer.Name + " from whitelist (" + whitelistedPlayer.IP + ")", ServerLog.MessageType.ConsoleUsage); + GameServer.Log(GameServer.ClientLogName(c) + " removed " + whitelistedPlayer.Name + " from whitelist (" + whitelistedPlayer.IP + ")", ServerLog.MessageType.ConsoleUsage); RemoveFromWhiteList(whitelistedPlayer); } } @@ -192,7 +192,7 @@ namespace Barotrauma.Networking string name = incMsg.ReadString(); string ip = incMsg.ReadString(); - GameServer.Log(c.Name + " added " + name + " to whitelist (" + ip + ")", ServerLog.MessageType.ConsoleUsage); + GameServer.Log(GameServer.ClientLogName(c) + " added " + name + " to whitelist (" + ip + ")", ServerLog.MessageType.ConsoleUsage); AddToWhiteList(name, ip); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Program.cs b/Barotrauma/BarotraumaServer/ServerSource/Program.cs index d7e8a5acc..e355d1d3d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Program.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Program.cs @@ -36,6 +36,7 @@ namespace Barotrauma AppDomain currentDomain = AppDomain.CurrentDomain; currentDomain.UnhandledException += new UnhandledExceptionEventHandler(CrashHandler); #endif + #if LINUX setLinuxEnv(); #endif @@ -97,7 +98,7 @@ namespace Barotrauma sb.AppendLine("Selected content packages: " + (!GameMain.SelectedPackages.Any() ? "None" : string.Join(", ", GameMain.SelectedPackages.Select(c => c.Name)))); } sb.AppendLine("Level seed: " + ((Level.Loaded == null) ? "no level loaded" : Level.Loaded.Seed)); - sb.AppendLine("Loaded submarine: " + ((Submarine.MainSub == null) ? "None" : Submarine.MainSub.Name + " (" + Submarine.MainSub.MD5Hash + ")")); + sb.AppendLine("Loaded submarine: " + ((Submarine.MainSub == null) ? "None" : Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash + ")")); sb.AppendLine("Selected screen: " + (Screen.Selected == null ? "None" : Screen.Selected.ToString())); if (GameMain.Server != null) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs index 5eb445456..be3523b4d 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Screens/NetLobbyScreen.cs @@ -8,10 +8,10 @@ namespace Barotrauma { partial class NetLobbyScreen : Screen { - private Submarine selectedSub; - private Submarine selectedShuttle; + private SubmarineInfo selectedSub; + private SubmarineInfo selectedShuttle; - public Submarine SelectedSub + public SubmarineInfo SelectedSub { get { return selectedSub; } set @@ -24,7 +24,7 @@ namespace Barotrauma } } } - public Submarine SelectedShuttle + public SubmarineInfo SelectedShuttle { get { return selectedShuttle; } set { selectedShuttle = value; lastUpdateID++; } @@ -121,7 +121,7 @@ namespace Barotrauma { LevelSeed = ToolBox.RandomSeed(8); - subs = Submarine.SavedSubmarines.Where(s => !s.HasTag(SubmarineTag.HideInMenus)).ToList(); + subs = SubmarineInfo.SavedSubmarines.Where(s => !s.HasTag(SubmarineTag.HideInMenus)).ToList(); if (subs == null || subs.Count() == 0) { @@ -150,8 +150,8 @@ namespace Barotrauma GameModes = GameModePreset.List.ToArray(); } - private List subs; - public List GetSubList() + private List subs; + public List GetSubList() { return subs; } @@ -198,7 +198,7 @@ namespace Barotrauma if (GameMain.Server.ServerSettings.SubSelectionMode == SelectionMode.Random) { - var nonShuttles = Submarine.SavedSubmarines.Where(c => !c.HasTag(SubmarineTag.Shuttle) && !c.HasTag(SubmarineTag.HideInMenus)).ToList(); + var nonShuttles = SubmarineInfo.SavedSubmarines.Where(c => !c.HasTag(SubmarineTag.Shuttle) && !c.HasTag(SubmarineTag.HideInMenus)).ToList(); SelectedSub = nonShuttles[Rand.Range(0, nonShuttles.Count)]; } if (GameMain.Server.ServerSettings.ModeSelectionMode == SelectionMode.Random) diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalEntityTransformation.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalEntityTransformation.cs index 2472d685f..5bfff56a5 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalEntityTransformation.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalEntityTransformation.cs @@ -67,7 +67,7 @@ namespace Barotrauma { continue; } - if (character.SpeciesName.ToLowerInvariant() == entities[activeEntityIndex] && Vector2.Distance(activeEntitySavedPosition, character.WorldPosition) < graceDistance) + if (character.SpeciesName.Equals(entities[activeEntityIndex], StringComparison.OrdinalIgnoreCase) && Vector2.Distance(activeEntitySavedPosition, character.WorldPosition) < graceDistance) { activeEntity = character; transformationTime = 0.0; @@ -117,7 +117,7 @@ namespace Barotrauma { continue; } - if (character.SpeciesName.ToLowerInvariant() == entities[activeEntityIndex].ToLowerInvariant()) + if (character.SpeciesName.Equals(entities[activeEntityIndex], StringComparison.OrdinalIgnoreCase)) { activeEntity = character; break; @@ -131,7 +131,7 @@ namespace Barotrauma { continue; } - if (item.prefab.Identifier.ToLowerInvariant() == entities[0].ToLowerInvariant()) + if (item.prefab.Identifier.Equals(entities[0], StringComparison.OrdinalIgnoreCase)) { activeEntity = item; break; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFloodPercentOfSub.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFloodPercentOfSub.cs index 414bc00ce..cceda84c4 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFloodPercentOfSub.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalFloodPercentOfSub.cs @@ -23,7 +23,7 @@ namespace Barotrauma var floodingAmount = 0.0f; foreach (Hull hull in Hull.hullList) { - if (hull.Submarine == null || hull.Submarine.IsOutpost || Traitors.All(traitor => hull.Submarine.TeamID != traitor.Character.TeamID)) { continue; } + if (hull.Submarine == null || hull.Submarine.Info.IsOutpost || Traitors.All(traitor => hull.Submarine.TeamID != traitor.Character.TeamID)) { continue; } if (hull.Submarine == GameMain.Server?.RespawnManager?.RespawnShuttle) { continue; } ++validHullsCount; floodingAmount += hull.WaterVolume / hull.Volume; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKeepTransformedAlive.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKeepTransformedAlive.cs index 6e40f2681..8d4500fa8 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKeepTransformedAlive.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/Goals/GoalKeepTransformedAlive.cs @@ -52,7 +52,7 @@ namespace Barotrauma { continue; } - if (character.SpeciesName.ToLowerInvariant() == speciesId) + if (character.SpeciesName.Equals(speciesId, StringComparison.OrdinalIgnoreCase)) { targetCharacter = character; break; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs index 730cadcde..5b26a8573 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Traitors/TraitorMission.cs @@ -235,7 +235,7 @@ namespace Barotrauma #if SERVER foreach (var traitor in Traitors.Values) { - GameServer.Log($"{traitor.Character.Name} is a traitor and the current goals are:\n{(traitor.CurrentObjective?.GoalInfos != null ? TextManager.GetServerMessage(traitor.CurrentObjective?.GoalInfos) : "(empty)")}", ServerLog.MessageType.ServerMessage); + GameServer.Log($"{GameServer.CharacterLogName(traitor.Character)} is a traitor and the current goals are:\n{(traitor.CurrentObjective?.GoalInfos != null ? TextManager.GetServerMessage(traitor.CurrentObjective?.GoalInfos) : "(empty)")}", ServerLog.MessageType.ServerMessage); } #endif return true; @@ -287,10 +287,7 @@ namespace Barotrauma { pendingObjectives.Remove(objective); completedObjectives.Add(objective); - if (pendingObjectives.Count > 0) - { - objective.EndMessage(); - } + objective.EndMessage(); continue; } if (objective.IsStarted && !objective.CanBeCompleted) diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 833e3b9a8..978f3cde6 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.9.8.0 + 0.9.9.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 7aea02e63..62415487a 100644 --- a/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml +++ b/Barotrauma/BarotraumaShared/Data/ContentPackages/Vanilla 0.9.xml @@ -51,7 +51,8 @@ - + + @@ -77,9 +78,16 @@ + + + + + + + @@ -93,6 +101,7 @@ + @@ -109,7 +118,10 @@ + + + @@ -136,6 +148,7 @@ + diff --git a/Barotrauma/BarotraumaShared/Data/karmasettings.xml b/Barotrauma/BarotraumaShared/Data/karmasettings.xml index a29ef9dd9..f358b0b39 100644 --- a/Barotrauma/BarotraumaShared/Data/karmasettings.xml +++ b/Barotrauma/BarotraumaShared/Data/karmasettings.xml @@ -22,7 +22,9 @@ kickbanthreshold="1" kicksbeforeban="3" karmanotificationinterval="15" - resetkarmabetweenrounds="true" /> + resetkarmabetweenrounds="true" + dangerousitemstealkarmadecrease="15" + dangerousitemstealbots="false" /> + resetkarmabetweenrounds="true" + dangerousitemstealkarmadecrease="15" + dangerousitemstealbots="true" /> + resetkarmabetweenrounds="true" + dangerousitemstealkarmadecrease="15" + dangerousitemstealbots="false" /> \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs index ae2b245fe..403f31dc0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/AITarget.cs @@ -177,7 +177,8 @@ namespace Barotrauma StaticSight = true; } SonarDisruption = element.GetAttributeFloat("sonardisruption", 0.0f); - SonarLabel = element.GetAttributeString("sonarlabel", ""); + string label = element.GetAttributeString("sonarlabel", ""); + SonarLabel = TextManager.Get(label, returnNull: true) ?? label; SonarIconIdentifier = element.GetAttributeString("sonaricon", ""); string typeString = element.GetAttributeString("type", "Any"); if (Enum.TryParse(typeString, out TargetType t)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 1d77645c4..4116a18e1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -36,14 +36,16 @@ namespace Barotrauma private readonly float updateTargetsInterval = 1; private readonly float updateMemoriesInverval = 1; + private readonly float attackLimbResetInterval = 2; - private float avoidLookAheadDistance; + private readonly float avoidLookAheadDistance; private SteeringManager outsideSteering, insideSteering; private float updateTargetsTimer; private float updateMemoriesTimer; - + private float attackLimbResetTimer; + private bool IsCoolDownRunning => AttackingLimb != null && AttackingLimb.attack.CoolDownTimer > 0; public float CombatStrength => Character.Params.AI.CombatStrength; private float Sight => Character.Params.AI.Sight; @@ -63,6 +65,7 @@ namespace Barotrauma get { return _attackingLimb; } private set { + attackLimbResetTimer = 0; _attackingLimb = value; attackVector = null; Reverse = _attackingLimb != null && _attackingLimb.attack.Reverse; @@ -376,7 +379,7 @@ namespace Barotrauma return; } float squaredDistance = Vector2.DistanceSquared(WorldPosition, SelectedAiTarget.WorldPosition); - var attackLimb = GetAttackLimb(SelectedAiTarget.WorldPosition); + var attackLimb = AttackingLimb ?? GetAttackLimb(SelectedAiTarget.WorldPosition); if (attackLimb != null && squaredDistance <= Math.Pow(attackLimb.attack.Range, 2)) { run = true; @@ -644,10 +647,14 @@ namespace Barotrauma var pickable = item.GetComponent(); if (pickable != null) { - var target = pickable.Picker?.AiTarget; - if (target?.Entity != null && !target.Entity.Removed) + Entity owner = pickable.Picker ?? item.ParentInventory?.Owner; + if (owner != null) { - SelectedAiTarget = target; + var target = owner.AiTarget; + if (target?.Entity != null && !target.Entity.Removed) + { + SelectedAiTarget = target; + } } } } @@ -685,7 +692,7 @@ namespace Barotrauma WallSection section = wall.Sections[i]; if (CanPassThroughHole(wall, i) && section?.gap != null) { - if (SteerThroughGap(wall, section, section.gap.WorldPosition, deltaTime)) + if (SteerThroughGap(wall, section, wall.SectionPosition(i, true), deltaTime)) { return; } @@ -980,7 +987,15 @@ namespace Barotrauma { // If not, reset the attacking limb, if the cooldown is not running // Don't use the property, because we don't want cancel reversing, if we are reversing. - _attackingLimb = null; + if (attackLimbResetTimer > attackLimbResetInterval) + { + _attackingLimb = null; + attackLimbResetTimer = 0; + } + else + { + attackLimbResetTimer += deltaTime; + } } } Limb steeringLimb = canAttack ? AttackingLimb : null; @@ -1045,7 +1060,8 @@ namespace Barotrauma } } // Steer towards the target if in the same room and swimming - if ((Character.AnimController.InWater || pursue) && targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull)) + if ((Character.AnimController.InWater || pursue || !Character.AnimController.CanWalk) && + (targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull) || Character.CanSeeTarget(SelectedAiTarget.Entity))) { SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(attackSimPos - steeringLimb.SimPosition)); } @@ -1112,31 +1128,13 @@ namespace Barotrauma return false; } - private bool CanAttack(Entity target) - { - if (target == null) { return false; } - if (target is Character c) - { - if (Character.CurrentHull == null && c.CurrentHull != null || Character.CurrentHull != null && c.CurrentHull == null) - { - return false; - } - } - else if (target is Item i && i.GetComponent() == null) - { - if (Character.CurrentHull == null && i.CurrentHull != null || Character.CurrentHull != null && i.CurrentHull == null) - { - return false; - } - } - return true; - } - + private readonly List attackLimbs = new List(); + private readonly List weights = new List(); private Limb GetAttackLimb(Vector2 attackWorldPos, Limb ignoredLimb = null) { var currentContexts = Character.GetAttackContexts(); Entity target = wallTarget != null ? wallTarget.Structure : SelectedAiTarget?.Entity; - if (!CanAttack(target)) { return null; } + if (target == null) { return null; } Limb selectedLimb = null; float currentPriority = -1; foreach (Limb limb in Character.AnimController.Limbs) @@ -1153,12 +1151,26 @@ namespace Barotrauma if (attack.Conditionals.Any(c => !c.Matches(se))) { continue; } } if (attack.Conditionals.Any(c => c.TargetSelf && !c.Matches(Character))) { continue; } - float priority = CalculatePriority(limb, attackWorldPos); - if (priority > currentPriority) + if (AIParams.RandomAttack) { - currentPriority = priority; - selectedLimb = limb; + attackLimbs.Add(limb); + weights.Add(limb.attack.Priority); } + else + { + float priority = CalculatePriority(limb, attackWorldPos); + if (priority > currentPriority) + { + currentPriority = priority; + selectedLimb = limb; + } + } + } + if (AIParams.RandomAttack) + { + selectedLimb = ToolBox.SelectWeightedRandom(attackLimbs, weights, Rand.RandSync.Server); + attackLimbs.Clear(); + weights.Clear(); } return selectedLimb; @@ -1176,18 +1188,22 @@ namespace Barotrauma { wallTarget = null; if (SelectedAiTarget == null) { return; } + if (SelectedAiTarget.Entity == null) { return; } //check if there's a wall between the target and the Character Vector2 rayStart = SimPosition; Vector2 rayEnd = SelectedAiTarget.SimPosition; - bool offset = SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null; - if (offset) + if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null) { rayStart -= SelectedAiTarget.Entity.Submarine.SimPosition; } + else if (SelectedAiTarget.Entity.Submarine == null && Character.Submarine != null) + { + rayEnd -= Character.Submarine.SimPosition; + } Body closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true, ignoreSensors: CanEnterSubmarine, ignoreDisabledWalls: CanEnterSubmarine); if (Submarine.LastPickedFraction != 1.0f && closestBody != null) { - if (closestBody.UserData is Structure wall && wall.Submarine != null) + if (closestBody.UserData is Structure wall && wall.Submarine != null && (wall.Submarine.Info.IsPlayer || wall.Submarine.Info.IsOutpost && TargetOutposts)) { int sectionIndex = wall.FindSectionIndex(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition)); float sectionDamage = wall.SectionDamage(sectionIndex); @@ -1265,20 +1281,26 @@ namespace Barotrauma Character.AnimController.ReleaseStuckLimbs(); LatchOntoAI?.DeattachFromBody(); if (attacker == null || attacker.AiTarget == null) { return; } + bool isFriendly = attacker.SpeciesName == Character.SpeciesName || attacker.Params.Group == Character.Params.Group; if (wasLatched) { avoidTimer = avoidTime * Rand.Range(0.75f, 1.25f); - SelectTarget(attacker.AiTarget); + if (!isFriendly) + { + SelectTarget(attacker.AiTarget); + } return; } if (State == AIState.Flee) { - SelectTarget(attacker.AiTarget); + if (!isFriendly) + { + SelectTarget(attacker.AiTarget); + } return; } - - if (attackResult.Damage > 0.0f) + if (!isFriendly && attackResult.Damage > 0.0f) { bool canAttack = attacker.Submarine == Character.Submarine && canAttackCharacters || attacker.Submarine != null && canAttackSub; if (Character.Params.AI.AttackWhenProvoked && canAttack) @@ -1325,13 +1347,20 @@ namespace Barotrauma ChangeTargetState(attacker, canAttack ? AIState.Attack : AIState.Escape, 100); } } + else if (canAttack && attacker.IsHuman && AIParams.TryGetTarget(attacker.SpeciesName, out CharacterParams.TargetParams targetingParams)) + { + if (targetingParams.State == AIState.Aggressive) + { + ChangeTargetState(attacker, AIState.Attack, 100); + } + } } AITargetMemory targetMemory = GetTargetMemory(attacker.AiTarget); targetMemory.Priority += GetRelativeDamage(attackResult.Damage, Character.Vitality) * AggressionHurt; // Only allow to react once. Otherwise would attack the target with only a fraction of a cooldown - bool retaliate = SelectedAiTarget != attacker.AiTarget && attacker.Submarine == Character.Submarine; + bool retaliate = !isFriendly && SelectedAiTarget != attacker.AiTarget && attacker.Submarine == Character.Submarine; bool avoidGunFire = Character.Params.AI.AvoidGunfire && attacker.Submarine != Character.Submarine; if (State == AIState.Attack && !IsCoolDownRunning) @@ -1376,6 +1405,8 @@ namespace Barotrauma IDamageable damageTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity as IDamageable; if (damageTarget != null) { + //simulate attack input to get the character to attack client-side + Character.SetInput(InputType.Attack, true, true); if (attackingLimb.UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance, targetLimb)) { if (damageTarget.Health > 0) @@ -1519,7 +1550,7 @@ namespace Barotrauma if (aiTarget.Type == AITarget.TargetType.HumanOnly) { continue; } if (!TargetOutposts) { - if (aiTarget.Entity.Submarine != null && aiTarget.Entity.Submarine.IsOutpost) { continue; } + if (aiTarget.Entity.Submarine != null && aiTarget.Entity.Submarine.Info.IsOutpost) { continue; } } Character targetCharacter = aiTarget.Entity as Character; //ignore the aitarget if it is the Character itself @@ -1529,30 +1560,6 @@ namespace Barotrauma string targetingTag = null; if (targetCharacter != null) { - if (targetCharacter.Submarine != Character.Submarine) - { - // In a different sub or the target is outside when we are inside or vice versa. - // Make an exception for humans so that creatures cannot avoid/flee from humans that are inside a sub. - if (!targetCharacter.IsHuman && State == AIState.Avoid && State == AIState.Escape & State == AIState.Flee) - { - // If we are escaping, let's not ignore the target entirely, because there can be a gaps where we or they can go freely - if (targetCharacter.Submarine != null) - { - // Target is inside -> reduce the priority - valueModifier *= 0.5f; - if (Character.Submarine != null) - { - // Both inside different submarines -> can ignore safely - continue; - } - } - } - else - { - // Don't attack targets that are not in the same submarine - continue; - } - } if (targetCharacter.IsDead) { targetingTag = "dead"; @@ -1604,9 +1611,11 @@ namespace Barotrauma } else if (aiTarget.Entity != null) { + // Ignore all structures and items inside wrecks + if (aiTarget.Entity.Submarine != null && aiTarget.Entity.Submarine.Info.IsWreck) { continue; } // Ignore the target if it's a room and the character is already inside a sub if (character.CurrentHull != null && aiTarget.Entity is Hull) { continue; } - + Door door = null; if (aiTarget.Entity is Item item) { @@ -1644,15 +1653,13 @@ namespace Barotrauma { continue; } - if (character.CurrentHull != null) + bool isCharacterOutside = s.Submarine == null || character.CurrentHull == null; + bool targetInnerWalls = AIParams.TargetInnerWalls; + if (!isCharacterOutside && !targetInnerWalls) { // Ignore walls when inside (walltargets still work) continue; } - if (s.Submarine == null) - { - continue; - } valueModifier = 1; if (!Character.AnimController.CanEnterSubmarine && IsWallDisabled(s)) { @@ -1665,28 +1672,43 @@ namespace Barotrauma bool leadsInside = !section.gap.IsRoomToRoom && section.gap.FlowTargetHull != null; if (Character.AnimController.CanEnterSubmarine) { - if (CanPassThroughHole(s, i)) + if (isCharacterOutside) { - valueModifier *= leadsInside ? (AggressiveBoarding ? 5 : 1) : 0; + if (CanPassThroughHole(s, i)) + { + valueModifier *= leadsInside ? (AggressiveBoarding ? 5 : 1) : (targetInnerWalls ? 1 : 0); + } + else + { + // Ignore holes that cannot be passed through if cannot attack items/structures. Holes that are big enough should be targeted, so that we can get in + if (!canAttackSub) + { + valueModifier = 0; + break; + } + if (AggressiveBoarding && leadsInside) + { + // Up to 100% priority increase for every gap in the wall when an aggressive boarder is outside + valueModifier *= 1 + section.gap.Open; + } + } } - else + else if (!canAttackSub || CanPassThroughHole(s, i)) { - // Ignore holes that cannot be passed through if cannot attack items/structures. Holes that are big enough should be targeted, so that we can get in - if (!canAttackSub) - { - valueModifier = 0; - break; - } - if (AggressiveBoarding) - { - // Up to 100% priority increase for every gap in the wall - valueModifier *= 1 + section.gap.Open; - } + // Already inside -> ignore holes in the walls and ignore walls if cannot attack the sub. + valueModifier = 0; + break; + } + else if (canAttackSub && !AggressiveBoarding) + { + // We are actually interested in breaking things -> reduce the priority when the wall is already broken + valueModifier *= 1 - section.gap.Open * 0.25f; } } - else if (!leadsInside) + else if (!leadsInside || !canAttackSub) { - // Ignore inner walls + // Can't get in, ignore inner walls + // Also ignore all walls if cannot attack the sub valueModifier = 0; break; } @@ -1774,6 +1796,50 @@ namespace Barotrauma // Don't target items that we own. // This is a rare case, and almost entirely related to Humanhusks, so let's check it last to reduce unnecessary checks (although the check shouldn't be expensive) if (aiTarget.Entity is Item i && i.IsOwnedBy(character)) { continue; } + if (targetCharacter != null) + { + if (targetCharacter.Submarine != Character.Submarine) + { + if (targetCharacter.Submarine != null) + { + // Target is inside -> reduce the priority + valueModifier *= 0.5f; + if (Character.Submarine != null) + { + // Both inside different submarines -> can ignore safely + continue; + } + } + else if (Character.CurrentHull != null) + { + // Target outside, but we are inside -> Check if we can get to the target. + // Only check if we are not already targeting the character. + // If we are, keep the target (unless we choose another). + if (SelectedAiTarget?.Entity != targetCharacter) + { + foreach (var gap in Character.CurrentHull.ConnectedGaps) + { + var door = gap.ConnectedDoor; + if (door == null || !door.IsOpen) + { + var wall = gap.ConnectedWall; + if (wall != null) + { + for (int j = 0; j < wall.Sections.Length; j++) + { + WallSection section = wall.Sections[j]; + if (!CanPassThroughHole(wall, j) && section?.gap != null) + { + continue; + } + } + } + } + } + } + } + } + } newTarget = aiTarget; selectedTargetMemory = targetMemory; targetValue = valueModifier; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs index 1fc0fa6cb..1cb0911e6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs @@ -74,7 +74,7 @@ namespace Barotrauma public override void Update(float deltaTime) { - if (DisableCrewAI || Character.IsUnconscious || Character.Removed) { return; } + if (DisableCrewAI || Character.IsIncapacitated || Character.Removed) { return; } base.Update(deltaTime); if (unreachableClearTimer > 0) @@ -139,7 +139,10 @@ namespace Barotrauma } if (Character.SpeechImpediment < 100.0f) { - ReportProblems(); + if (Character.Submarine != null && Character.Submarine.TeamID == Character.TeamID && !Character.Submarine.Info.IsWreck) + { + ReportProblems(); + } UpdateSpeaking(); } UnequipUnnecessaryItems(); @@ -1049,12 +1052,59 @@ namespace Barotrauma private static bool FilterCrewMember(Character self, Character other) => other != null && !other.IsDead && !other.Removed && other.AIController is HumanAIController humanAi && humanAi.IsFriendly(self); + public static bool IsItemOperatedByAnother(Character character, ItemComponent target, out Character operatingCharacter) + { + operatingCharacter = null; + if (target?.Item == null) { return false; } + foreach (var c in Character.CharacterList) + { + if (character != null && c == character) { continue; } + if (character?.AIController is HumanAIController humanAi && !humanAi.IsFriendly(c)) { continue; } + if (c.SelectedConstruction != target.Item) { continue; } + operatingCharacter = c; + // If the other character is player, don't try to operate + if (c.IsRemotePlayer || Character.Controlled == c) { return true; } + if (c.AIController is HumanAIController controllingHumanAi) + { + // If the other character is ordered to operate the item, let him do it + if (controllingHumanAi.ObjectiveManager.IsCurrentOrder()) + { + return true; + } + else + { + if (character == null) + { + return true; + } + else if (target is Steering) + { + // Steering is hard-coded -> cannot use the required skills collection defined in the xml + return character.GetSkillLevel("helm") <= c.GetSkillLevel("helm"); + } + else + { + return target.DegreeOfSuccess(character) <= target.DegreeOfSuccess(c); + } + } + } + else + { + // Shouldn't go here, unless we allow non-humans to operate items + return false; + } + + } + return false; + } + #region Wrappers public bool IsFriendly(Character other) => IsFriendly(Character, other); public void DoForEachCrewMember(Action action) => DoForEachCrewMember(Character, action); public bool IsTrueForAnyCrewMember(Func predicate) => IsTrueForAnyCrewMember(Character, predicate); public bool IsTrueForAllCrewMembers(Func predicate) => IsTrueForAllCrewMembers(Character, predicate); public int CountCrew(Func predicate = null, bool onlyActive = true, bool onlyBots = false) => CountCrew(Character, predicate, onlyActive, onlyBots); + public bool IsItemOperatedByAnother(ItemComponent target, out Character operatingCharacter) => IsItemOperatedByAnother(Character, target, out operatingCharacter); #endregion } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs index ece8f6cf3..c5ab0ea33 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/NPCConversation.cs @@ -136,9 +136,10 @@ namespace Barotrauma string allowedJobsStr = element.GetAttributeString("allowedjobs", ""); foreach (string allowedJobIdentifier in allowedJobsStr.Split(',')) { - if (JobPrefab.Prefabs.ContainsKey(allowedJobIdentifier.ToLowerInvariant())) + string key = allowedJobIdentifier.ToLowerInvariant(); + if (JobPrefab.Prefabs.ContainsKey(key)) { - AllowedJobs.Add(JobPrefab.Prefabs[allowedJobIdentifier.ToLowerInvariant()]); + AllowedJobs.Add(JobPrefab.Prefabs[key]); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs index bf057ce1a..bf6fa27d1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjective.cs @@ -30,6 +30,7 @@ namespace Barotrauma public virtual bool KeepDivingGearOn => false; public virtual bool UnequipItems => false; + public virtual bool AllowOutsideSubmarine => false; protected readonly List subObjectives = new List(); private float _cumulatedDevotion; @@ -182,11 +183,18 @@ namespace Barotrauma } } + protected bool IsAllowed => AllowOutsideSubmarine || character.Submarine != null && character.Submarine.TeamID == character.TeamID && character.Submarine.Info.IsPlayer; + /// /// Call this only when the priority needs to be recalculated. Use the cached Priority property when you don't need to recalculate. /// public virtual float GetPriority() { + if (!IsAllowed) + { + Priority = 0; + return Priority; + } if (objectiveManager.CurrentOrder == this) { Priority = AIObjectiveManager.OrderPriority; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs index 5defc9589..e45d30e08 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveChargeBatteries.cs @@ -23,8 +23,12 @@ namespace Barotrauma if (item.Submarine == null) { return false; } if (item.CurrentHull == null) { return false; } if (item.Submarine.TeamID != character.TeamID) { return false; } + if (character.Submarine != null) + { + if (item.Submarine.Info.Type != character.Submarine.Info.Type) { return false; } + if (!character.Submarine.IsEntityFoundOnThisSub(item, true)) { return false; } + } if (item.ConditionPercentage <= 0) { return false; } - if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { return false; } if (Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; } if (IsReady(battery)) { return false; } return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index 82b80055c..f454f4704 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -13,6 +13,7 @@ namespace Barotrauma public override bool KeepDivingGearOn => true; public override bool IgnoreUnsafeHulls => true; + public override bool AllowOutsideSubmarine => true; private readonly CombatMode initialMode; @@ -149,12 +150,12 @@ namespace Barotrauma } if (!HoldPosition && seekAmmunition == null) { - Move(); + Move(deltaTime); } } } - private void Move() + private void Move(float deltaTime) { switch (Mode) { @@ -163,7 +164,7 @@ namespace Barotrauma break; case CombatMode.Defensive: case CombatMode.Retreat: - Retreat(); + Retreat(deltaTime); break; default: throw new NotImplementedException(); @@ -411,7 +412,10 @@ namespace Barotrauma return true; } - private void Retreat() + private float findHullTimer; + private readonly float findHullInterval = 1.0f; + + private void Retreat(float deltaTime) { RemoveSubObjective(ref followTargetObjective); RemoveSubObjective(ref seekAmmunition); @@ -421,7 +425,15 @@ namespace Barotrauma } if (retreatTarget == null || (retreatObjective != null && !retreatObjective.CanBeCompleted)) { - retreatTarget = findSafety.FindBestHull(HumanAIController.VisibleHulls); + if (findHullTimer > 0) + { + findHullTimer -= deltaTime; + } + else + { + retreatTarget = findSafety.FindBestHull(HumanAIController.VisibleHulls); + findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f); + } } if (retreatTarget != null && character.CurrentHull != retreatTarget) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs index 8ccdf33c8..9b59a27a4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFire.cs @@ -27,6 +27,11 @@ namespace Barotrauma public override float GetPriority() { + if (!IsAllowed) + { + Priority = 0; + return Priority; + } if (!objectiveManager.IsCurrentOrder() && Character.CharacterList.Any(c => c.CurrentHull == targetHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs index fd6bc49ce..e41f6675f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveExtinguishFires.cs @@ -32,7 +32,11 @@ namespace Barotrauma if (hull.FireSources.None()) { return false; } if (hull.Submarine == null) { return false; } if (hull.Submarine.TeamID != character.TeamID) { return false; } - if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(hull, true)) { return false; } + if (character.Submarine != null) + { + if (hull.Submarine.Info.Type != character.Submarine.Info.Type) { return false; } + if (!character.Submarine.IsEntityFoundOnThisSub(hull, true)) { return false; } + } return true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs index a21460a6d..38a791318 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFightIntruders.cs @@ -40,7 +40,11 @@ namespace Barotrauma if (target.Submarine == null) { return false; } if (target.Submarine.TeamID != character.TeamID) { return false; } if (target.CurrentHull == null) { return false; } - if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, true)) { return false; } + if (character.Submarine != null) + { + if (target.Submarine.Info.Type != character.Submarine.Info.Type) { return false; } + if (!character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, true)) { return false; } + } return true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs index 87732b7b0..f3c677359 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs @@ -12,6 +12,7 @@ namespace Barotrauma public override bool KeepDivingGearOn => true; public override bool IgnoreUnsafeHulls => true; public override bool ConcurrentObjectives => true; + public override bool AllowOutsideSubmarine => true; public override bool IsLoop { get => true; set => throw new System.Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace); } // TODO: expose? @@ -35,6 +36,11 @@ namespace Barotrauma public override float GetPriority() { + if (!IsAllowed) + { + Priority = 0; + return Priority; + } if (character.CurrentHull == null) { Priority = objectiveManager.CurrentOrder is AIObjectiveGoTo ? 0 : 100; @@ -77,7 +83,7 @@ namespace Barotrauma else { float dangerFactor = (100 - currenthullSafety) / 100; - Priority += dangerFactor * priorityIncrease * deltaTime; + Priority = Math.Min(Priority + dangerFactor * priorityIncrease * deltaTime, 100); } } } @@ -100,7 +106,7 @@ namespace Barotrauma if (needsEquipment && divingGearObjective == null && !character.LockHands) { RemoveSubObjective(ref goToObjective); - TryAddSubObjective(ref divingGearObjective, + TryAddSubObjective(ref divingGearObjective, constructor: () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager), onAbandon: () => { @@ -139,14 +145,14 @@ namespace Barotrauma { RemoveSubObjective(ref goToObjective); } - TryAddSubObjective(ref goToObjective, + TryAddSubObjective(ref goToObjective, constructor: () => new AIObjectiveGoTo(currentSafeHull, character, objectiveManager, getDivingGearIfNeeded: true) { AllowGoingOutside = HumanAIController.HasDivingSuit(character, conditionPercentage: 50) }, onCompleted: () => { - if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD || + if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD || HumanAIController.NeedsDivingGear(character, currentHull, out bool needsSuit) && (needsSuit ? HumanAIController.HasDivingSuit(character) : HumanAIController.HasDivingMask(character))) { resetPriority = true; @@ -244,10 +250,8 @@ namespace Barotrauma //(no need to do the expensive pathfinding if we already know we're not going to choose this hull) if (hullSafety < bestValue) { continue; } // Don't allow to go outside if not already outside. - var path = character.CurrentHull != null ? - PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition, nodeFilter: node => node.Waypoint.CurrentHull != null) : - PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition); - if (path.Unreachable && character.CurrentHull != null) + var path = PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition, nodeFilter: node => node.Waypoint.CurrentHull != null); + if (path.Unreachable) { HumanAIController.UnreachableHulls.Add(hull); continue; @@ -265,7 +269,7 @@ namespace Barotrauma else { // Outside - if (hull.RoomName != null && hull.RoomName.ToLowerInvariant().Contains("airlock")) + if (hull.RoomName != null && hull.RoomName.Contains("airlock", StringComparison.OrdinalIgnoreCase)) { hullSafety = 100; } @@ -287,6 +291,7 @@ namespace Barotrauma float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, MathUtils.Pow(100000, 2), distance)); hullSafety *= distanceFactor; // If the target is not inside a friendly submarine, considerably reduce the hull safety. + // Intentionally exclude wrecks from this check if (hull.Submarine.TeamID != character.TeamID && hull.Submarine.TeamID != Character.TeamType.FriendlyNPC) { hullSafety /= 10; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs index 4121ccdce..0ba7d3323 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeak.cs @@ -1,9 +1,9 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Linq; -using Barotrauma.Extensions; namespace Barotrauma { @@ -20,15 +20,23 @@ namespace Barotrauma private AIObjectiveGoTo gotoObjective; private AIObjectiveOperateItem operateObjective; - public AIObjectiveFixLeak(Gap leak, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base (character, objectiveManager, priorityModifier) + public bool IgnoreSeverityAndDistance { get; private set; } + + public AIObjectiveFixLeak(Gap leak, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool ignoreSeverityAndDistance = false) : base (character, objectiveManager, priorityModifier) { Leak = leak; + IgnoreSeverityAndDistance = ignoreSeverityAndDistance; } protected override bool Check() => Leak.Open <= 0 || Leak.Removed; public override float GetPriority() { + if (!IsAllowed) + { + Priority = 0; + return Priority; + } if (Leak.Removed || Leak.Open <= 0) { Priority = 0; @@ -39,8 +47,8 @@ namespace Barotrauma float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y); // Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally). // If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby. - float distanceFactor = xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, xDist + yDist * 3.0f)); - float severity = AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100; + float distanceFactor = IgnoreSeverityAndDistance || xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, xDist + yDist * 3.0f)); + float severity = IgnoreSeverityAndDistance ? 1 : AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100; float max = Math.Min((AIObjectiveManager.OrderPriority - 1), 90); float devotion = CumulatedDevotion / 100; Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs index 659958632..e58862a03 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFixLeaks.cs @@ -1,7 +1,5 @@ using Microsoft.Xna.Framework; -using System; using System.Linq; -using Barotrauma.Extensions; using System.Collections.Generic; namespace Barotrauma @@ -11,8 +9,12 @@ namespace Barotrauma public override string DebugTag => "fix leaks"; public override bool ForceRun => true; public override bool KeepDivingGearOn => true; + private Hull PrioritizedHull { get; set; } - public AIObjectiveFixLeaks(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { } + public AIObjectiveFixLeaks(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, Hull prioritizedHull = null) : base(character, objectiveManager, priorityModifier) + { + PrioritizedHull = prioritizedHull; + } protected override bool Filter(Gap gap) => IsValidTarget(gap, character); @@ -60,7 +62,7 @@ namespace Barotrauma protected override IEnumerable GetList() => Gap.GapList; protected override AIObjective ObjectiveConstructor(Gap gap) - => new AIObjectiveFixLeak(gap, character, objectiveManager, PriorityModifier); + => new AIObjectiveFixLeak(gap, character, objectiveManager, priorityModifier: PriorityModifier, ignoreSeverityAndDistance: gap.FlowTargetHull == PrioritizedHull); protected override void OnObjectiveCompleted(AIObjective objective, Gap target) => HumanAIController.RemoveTargets(character, target); @@ -71,7 +73,11 @@ namespace Barotrauma if (gap.ConnectedWall == null || gap.ConnectedDoor != null || gap.Open <= 0 || gap.linkedTo.All(l => l == null)) { return false; } if (gap.Submarine == null) { return false; } if (gap.Submarine.TeamID != character.TeamID) { return false; } - if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(gap, true)) { return false; } + if (character.Submarine != null) + { + if (gap.Submarine.Info.Type != character.Submarine.Info.Type) { return false; } + if (!character.Submarine.IsEntityFoundOnThisSub(gap, true)) { return false; } + } return true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs index 626c49064..14f746626 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs @@ -220,7 +220,11 @@ namespace Barotrauma { if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; } } - if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { continue; } + if (character.Submarine != null) + { + if (item.Submarine.Info.Type != character.Submarine.Info.Type) { continue; } + if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { continue; } + } if (character.IsItemTakenBySomeoneElse(item)) { continue; } float itemPriority = 1; if (GetItemPriority != null) @@ -276,6 +280,7 @@ namespace Barotrauma private bool CheckItem(Item item) { + if (item.NonInteractable) { return false; } if (ignoredItems.Contains(item)) { return false; }; if (item.Condition < TargetCondition) { return false; } if (ItemFilter != null && !ItemFilter(item)) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs index 384c7dade..e2f0577a2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs @@ -44,6 +44,9 @@ namespace Barotrauma public bool AllowGoingOutside { get; set; } public override bool AbandonWhenCannotCompleteSubjectives => !repeat; + + public override bool AllowOutsideSubmarine => AllowGoingOutside; + public string DialogueIdentifier { get; set; } public string TargetName { get; set; } @@ -65,13 +68,13 @@ namespace Barotrauma } else { - return base.GetPriority(); + Priority = objectiveManager.CurrentOrder == this ? AIObjectiveManager.OrderPriority : 10; } return Priority; } - public AIObjectiveGoTo(ISpatialEntity target, Character character, AIObjectiveManager objectiveManager, bool repeat = false, bool getDivingGearIfNeeded = true, float priorityModifier = 1, float closeEnough = 0) - : base (character, objectiveManager, priorityModifier) + public AIObjectiveGoTo(ISpatialEntity target, Character character, AIObjectiveManager objectiveManager, bool repeat = false, bool getDivingGearIfNeeded = true, float priorityModifier = 1, float closeEnough = 0) + : base(character, objectiveManager, priorityModifier) { this.Target = target; this.repeat = repeat; @@ -204,7 +207,7 @@ namespace Barotrauma } if (needsEquipment) { - TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager), + TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager), onAbandon: () => Abandon = true, onCompleted: () => RemoveSubObjective(ref findDivingGear)); return; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs index 5a62a53bb..2a2a9be3b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs @@ -11,6 +11,7 @@ namespace Barotrauma { public override string DebugTag => "idle"; public override bool UnequipItems => true; + public override bool AllowOutsideSubmarine => true; private readonly float newTargetIntervalMin = 10; private readonly float newTargetIntervalMax = 20; @@ -231,9 +232,11 @@ namespace Barotrauma { if (HumanAIController.UnsafeHulls.Contains(hull)) { continue; } if (hull.Submarine == null) { continue; } - if (hull.Submarine.TeamID != character.TeamID) { continue; } - // If the character is inside, only take connected hulls into account. - if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(hull, true)) { continue; } + if (character.Submarine == null) { break; } + if (hull.Submarine.TeamID != character.Submarine.TeamID) { continue; } + if (hull.Submarine.Info.Type != character.Submarine.Info.Type) { continue; } + // If the character is inside, only take connected subs into account. + if (!character.Submarine.IsEntityFoundOnThisSub(hull, true)) { continue; } if (IsForbidden(hull)) { continue; } // Ignore hulls that are too low to stand inside if (character.AnimController is HumanoidAnimController animController) @@ -261,9 +264,9 @@ namespace Barotrauma public static bool IsForbidden(Hull hull) { if (hull == null) { return true; } - string hullName = hull.RoomName?.ToLowerInvariant(); + string hullName = hull.RoomName; if (hullName == null) { return false; } - return hullName.Contains("ballast") || hullName.Contains("airlock"); + return hullName.Contains("ballast", StringComparison.OrdinalIgnoreCase) || hullName.Contains("airlock", StringComparison.OrdinalIgnoreCase); } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs index 8c2a4b340..d2c3f3a6a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveLoop.cs @@ -108,6 +108,11 @@ namespace Barotrauma public override float GetPriority() { + if (!IsAllowed) + { + Priority = 0; + return Priority; + } if (character.LockHands || character.Submarine == null || Targets.None()) { Priority = 0; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs index ff06d4eb2..8da13ea1e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveManager.cs @@ -1,9 +1,10 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using Barotrauma.Extensions; -using Microsoft.Xna.Framework; namespace Barotrauma { @@ -102,20 +103,12 @@ namespace Barotrauma { var orderPrefab = Order.GetPrefab(automaticOrder.identifier); if (orderPrefab == null) { throw new Exception($"Could not find a matching prefab by the identifier: '{automaticOrder.identifier}'"); } - // TODO: Similar code is used in CrewManager:815-> DRY - var matchingItems = orderPrefab.ItemIdentifiers.Any() ? - Item.ItemList.FindAll(it => orderPrefab.ItemIdentifiers.Contains(it.Prefab.Identifier) || it.HasTag(orderPrefab.ItemIdentifiers)) : - Item.ItemList.FindAll(it => it.Components.Any(ic => ic.GetType() == orderPrefab.ItemComponentType)); - matchingItems.RemoveAll(it => it.Submarine != character.Submarine); - var item = matchingItems.GetRandom(); - var order = new Order( - orderPrefab, - item ?? character.CurrentHull as Entity, - item?.Components.FirstOrDefault(ic => ic.GetType() == orderPrefab.ItemComponentType), - orderGiver: character); + var item = orderPrefab.MustSetTarget ? orderPrefab.GetMatchingItems(character.Submarine, false)?.GetRandom() : null; + var order = new Order(orderPrefab, item ?? character.CurrentHull as Entity, + item?.Components.FirstOrDefault(ic => ic.GetType() == orderPrefab.ItemComponentType), orderGiver: character); if (order == null) { continue; } var objective = CreateObjective(order, automaticOrder.option, character, automaticOrder.priorityModifier); - if (objective != null) + if (objective != null && objective.CanBeCompleted) { AddObjective(objective, delay: Rand.Value() / 2); objectiveCount++; @@ -220,7 +213,7 @@ namespace Barotrauma } GetCurrentObjective()?.SortSubObjectives(); } - + public void DoCurrentObjective(float deltaTime) { if (WaitTimer <= 0) @@ -232,7 +225,7 @@ namespace Barotrauma character.AIController.SteeringManager.Reset(); } } - + public void SetOrder(AIObjective objective) { CurrentOrder = objective; @@ -271,13 +264,13 @@ namespace Barotrauma }; break; case "wait": - newObjective = new AIObjectiveGoTo(character, character, this, repeat: true, priorityModifier: priorityModifier) + newObjective = new AIObjectiveGoTo(order.TargetEntity ?? character, character, this, repeat: true, priorityModifier: priorityModifier) { AllowGoingOutside = character.CurrentHull == null }; break; case "fixleaks": - newObjective = new AIObjectiveFixLeaks(character, this, priorityModifier); + newObjective = new AIObjectiveFixLeaks(character, this, priorityModifier: priorityModifier, prioritizedHull: order.TargetEntity as Hull); break; case "chargebatteries": newObjective = new AIObjectiveChargeBatteries(character, this, option, priorityModifier); @@ -286,13 +279,24 @@ namespace Barotrauma newObjective = new AIObjectiveRescueAll(character, this, priorityModifier); break; case "repairsystems": - newObjective = new AIObjectiveRepairItems(character, this, priorityModifier) + case "repairmechanical": + case "repairelectrical": + newObjective = new AIObjectiveRepairItems(character, this, priorityModifier: priorityModifier, prioritizedItem: order.TargetEntity as Item) { + RelevantSkill = order.AppropriateSkill, RequireAdequateSkills = option == "jobspecific" }; break; case "pumpwater": - newObjective = new AIObjectivePumpWater(character, this, option, priorityModifier: priorityModifier); + if (order.TargetItemComponent is Pump targetPump) + { + newObjective = new AIObjectiveOperateItem(targetPump, character, this, option, false, priorityModifier: priorityModifier); + // newObjective.Completed += DismissSelf; + } + else + { + newObjective = new AIObjectivePumpWater(character, this, option, priorityModifier: priorityModifier); + } break; case "extinguishfires": newObjective = new AIObjectiveExtinguishFires(character, this, priorityModifier); @@ -324,6 +328,18 @@ namespace Barotrauma return newObjective; } + private void DismissSelf() + { +#if CLIENT + if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer) + { + GameMain.GameSession?.CrewManager?.SetCharacterOrder(character, Order.GetPrefab("dismissed"), null, character); + } +#else + GameMain.Server?.SendOrderChatMessage(new OrderChatMessage(Order.GetPrefab("dismissed"), null, null, character, character)); +#endif + } + private bool IsAllowedToWait() { if (CurrentOrder != null) { return false; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs index 0c00d3e96..73d597ec5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveOperateItem.cs @@ -32,6 +32,11 @@ namespace Barotrauma public override float GetPriority() { + if (!IsAllowed) + { + Priority = 0; + return Priority; + } if (component.Item.ConditionPercentage <= 0) { Priority = 0; @@ -42,11 +47,21 @@ namespace Barotrauma { Priority = AIObjectiveManager.OrderPriority; } - if (component.Item.CurrentHull == null || component.Item.CurrentHull.FireSources.Any() || IsOperatedByAnother(character, GetTarget(), out _)) + Item targetItem = GetTarget()?.Item; + if (targetItem == null) + { +#if DEBUG + DebugConsole.ThrowError("Item or component of AI Objective Operate item wass null. This shouldn't happen."); +#endif + Abandon = true; + Priority = 0; + return 0.0f; + } + if (targetItem.CurrentHull == null || targetItem.CurrentHull.FireSources.Any() || HumanAIController.IsItemOperatedByAnother(GetTarget(), out _)) { Priority = 0; } - else if (Character.CharacterList.Any(c => c.CurrentHull == component.Item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) + else if (Character.CharacterList.Any(c => c.CurrentHull == targetItem.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { Priority = 0; } @@ -60,8 +75,8 @@ namespace Barotrauma return Priority; } - public AIObjectiveOperateItem(ItemComponent item, Character character, AIObjectiveManager objectiveManager, string option, bool requireEquip, Entity operateTarget = null, bool useController = false, float priorityModifier = 1) - : base (character, objectiveManager, priorityModifier, option) + public AIObjectiveOperateItem(ItemComponent item, Character character, AIObjectiveManager objectiveManager, string option, bool requireEquip, Entity operateTarget = null, bool useController = false, float priorityModifier = 1) + : base(character, objectiveManager, priorityModifier, option) { this.component = item ?? throw new System.ArgumentNullException("item", "Attempted to create an AIObjectiveOperateItem with a null target."); this.requireEquip = requireEquip; @@ -76,51 +91,6 @@ namespace Barotrauma } } - public static bool IsOperatedByAnother(Character character, ItemComponent target, out Character operatingCharacter) - { - operatingCharacter = null; - foreach (var c in Character.CharacterList) - { - if (character != null && c == character) { continue; } - if (character?.AIController is HumanAIController humanAi && !humanAi.IsFriendly(c)) { continue; } - if (c.SelectedConstruction != target.Item) { continue; } - operatingCharacter = c; - // If the other character is player, don't try to operate - if (c.IsRemotePlayer || Character.Controlled == c) { return true; } - if (c.AIController is HumanAIController controllingHumanAi) - { - // If the other character is ordered to operate the item, let him do it - if (controllingHumanAi.ObjectiveManager.IsCurrentOrder()) - { - return true; - } - else - { - if (character == null) - { - return true; - } - else if (target is Steering) - { - // Steering is hard-coded -> cannot use the required skills collection defined in the xml - return character.GetSkillLevel("helm") <= c.GetSkillLevel("helm"); - } - else - { - return target.DegreeOfSuccess(character) <= target.DegreeOfSuccess(c); - } - } - } - else - { - // Shouldn't go here, unless we allow non-humans to operate items - return false; - } - - } - return false; - } - protected override void Act(float deltaTime) { if (character.LockHands) @@ -136,7 +106,7 @@ namespace Barotrauma return; } // Don't allow to operate an item that someone with a better skills already operates, unless this is an order - if (objectiveManager.CurrentOrder != this && IsOperatedByAnother(character, target, out _)) + if (objectiveManager.CurrentOrder != this && HumanAIController.IsItemOperatedByAnother(target, out _)) { // Don't abandon return; @@ -161,7 +131,7 @@ namespace Barotrauma { DialogueIdentifier = "dialogcannotreachtarget", TargetName = target.Item.Name - }, + }, onAbandon: () => Abandon = true, onCompleted: () => RemoveSubObjective(ref goToObjective)); } @@ -176,7 +146,7 @@ namespace Barotrauma } else if (!character.Inventory.Items.Contains(component.Item)) { - TryAddSubObjective(ref getItemObjective, () => new AIObjectiveGetItem(character, component.Item, objectiveManager, equip: true), + TryAddSubObjective(ref getItemObjective, () => new AIObjectiveGetItem(character, component.Item, objectiveManager, equip: true), onAbandon: () => Abandon = true, onCompleted: () => RemoveSubObjective(ref getItemObjective)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs index 0921dc1ff..7e50b0738 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectivePumpWater.cs @@ -33,7 +33,11 @@ namespace Barotrauma if (pump.Item.Submarine.TeamID != character.TeamID) { return false; } if (pump.Item.ConditionPercentage <= 0) { return false; } if (pump.Item.CurrentHull.FireSources.Count > 0) { return false; } - if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(pump.Item, true)) { return false; } + if (character.Submarine != null) + { + if (pump.Item.Submarine.Info.Type != character.Submarine.Info.Type) { return false; } + if (!character.Submarine.IsEntityFoundOnThisSub(pump.Item, true)) { return false; } + } if (Character.CharacterList.Any(c => c.CurrentHull == pump.Item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; } if (IsReady(pump)) { return false; } return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index adcaf7124..b01e35deb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -20,14 +20,22 @@ namespace Barotrauma private RepairTool repairTool; private bool IsRepairing => character.SelectedConstruction == Item && Item.GetComponent()?.CurrentFixer == character; + private readonly bool isPriority; - public AIObjectiveRepairItem(Character character, Item item, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) + public AIObjectiveRepairItem(Character character, Item item, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool isPriority = false) + : base(character, objectiveManager, priorityModifier) { Item = item; + this.isPriority = isPriority; } public override float GetPriority() { + if (!IsAllowed) + { + Priority = 0; + return Priority; + } // TODO: priority list? // Ignore items that are being repaired by someone else. if (Item.Repairables.Any(r => r.CurrentFixer != null && r.CurrentFixer != character)) @@ -36,16 +44,16 @@ namespace Barotrauma } else { - float yDist = Math.Abs(character.WorldPosition.Y - Item.WorldPosition.Y); - yDist = yDist > 100 ? yDist * 5 : 0; - float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + yDist; - float distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 5000, dist)); - if (Item.CurrentHull == character.CurrentHull) + float distanceFactor = 1; + if (!isPriority && Item.CurrentHull != character.CurrentHull) { - distanceFactor = 1; + float yDist = Math.Abs(character.WorldPosition.Y - Item.WorldPosition.Y); + yDist = yDist > 100 ? yDist * 5 : 0; + float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + yDist; + distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 5000, dist)); } - float damagePriority = MathHelper.Lerp(1, 0, Item.Condition / Item.MaxCondition); - float successFactor = MathHelper.Lerp(0, 1, Item.Repairables.Average(r => r.DegreeOfSuccess(character))); + float damagePriority = isPriority ? 1 : MathHelper.Lerp(1, 0, Item.Condition / Item.MaxCondition); + float successFactor = isPriority ? 1 : MathHelper.Lerp(0, 1, Item.Repairables.Average(r => r.DegreeOfSuccess(character))); float isSelected = IsRepairing ? 50 : 0; float devotion = (CumulatedDevotion + isSelected) / 100; float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90); @@ -165,7 +173,7 @@ namespace Barotrauma } repairable.StopRepairing(character); } - else + else if (repairable.CurrentFixer != character) { repairable.StartRepairing(character, Repairable.FixActions.Repair); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs index f6df24810..1a7867cd1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItems.cs @@ -15,12 +15,23 @@ namespace Barotrauma /// public bool RequireAdequateSkills; + /// + /// If set, only fix items where required skill matches this. + /// + public string RelevantSkill; + + private readonly Item prioritizedItem; + public override bool AllowMultipleInstances => true; public override bool IsDuplicate(T otherObjective) => (otherObjective as AIObjective) is AIObjectiveRepairItems repairObjective && repairObjective.RequireAdequateSkills == RequireAdequateSkills; - public AIObjectiveRepairItems(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { } + public AIObjectiveRepairItems(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, Item prioritizedItem = null) + : base(character, objectiveManager, priorityModifier) + { + this.prioritizedItem = prioritizedItem; + } protected override void CreateObjectives() { @@ -71,6 +82,10 @@ namespace Barotrauma { if (item.Repairables.Any(r => !r.HasRequiredSkills(character))) { return false; } } + if (!string.IsNullOrWhiteSpace(RelevantSkill)) + { + if (item.Repairables.None(r => r.requiredSkills.Any(s => s.Identifier.Equals(RelevantSkill, StringComparison.OrdinalIgnoreCase)))) { return false; } + } return true; } @@ -103,7 +118,7 @@ namespace Barotrauma protected override IEnumerable GetList() => Item.ItemList; protected override AIObjective ObjectiveConstructor(Item item) - => new AIObjectiveRepairItem(character, item, objectiveManager, PriorityModifier); + => new AIObjectiveRepairItem(character, item, objectiveManager, priorityModifier: PriorityModifier, isPriority: item == prioritizedItem); protected override void OnObjectiveCompleted(AIObjective objective, Item target) => HumanAIController.RemoveTargets(character, target); @@ -116,7 +131,11 @@ namespace Barotrauma if (item.Submarine == null) { return false; } if (item.Submarine.TeamID != character.TeamID) { return false; } if (item.Repairables.None()) { return false; } - if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(item, true)) { return false; } + if (character.Submarine != null) + { + if (item.Submarine.Info.Type != character.Submarine.Info.Type) { return false; } + if (!character.Submarine.IsEntityFoundOnThisSub(item, true)) { return false; } + } return true; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs index 57cea1a9f..c7e58a41d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescue.cs @@ -22,6 +22,8 @@ namespace Barotrauma private AIObjectiveGetItem getItemObjective; private float treatmentTimer; private Hull safeHull; + private float findHullTimer; + private readonly float findHullInterval = 1.0f; public AIObjectiveRescue(Character character, Character targetCharacter, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) @@ -56,14 +58,17 @@ namespace Barotrauma if (targetCharacter != character) { - // Unconcious target is not in a safe place -> Move to a safe place first - if (targetCharacter.IsUnconscious && HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD) + // Incapacitated target is not in a safe place -> Move to a safe place first + if (targetCharacter.IsIncapacitated && HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD) { if (character.SelectedCharacter != targetCharacter) { - character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget", new string[2] { "[targetname]", "[roomname]" }, - new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }), - null, 1.0f, "foundunconscioustarget" + targetCharacter.Name, 60.0f); + if (targetCharacter.CurrentHull.DisplayName != null) + { + character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget", new string[2] { "[targetname]", "[roomname]" }, + new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }), + null, 1.0f, "foundunconscioustarget" + targetCharacter.Name, 60.0f); + } // Go to the target and select it if (!character.CanInteractWith(targetCharacter)) @@ -92,9 +97,17 @@ namespace Barotrauma // Drag the character into safety if (safeHull == null) { - safeHull = objectiveManager.GetObjective().FindBestHull(HumanAIController.VisibleHulls); + if (findHullTimer > 0) + { + findHullTimer -= deltaTime; + } + else + { + safeHull = objectiveManager.GetObjective().FindBestHull(HumanAIController.VisibleHulls); + findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f); + } } - if (character.CurrentHull != safeHull) + if (safeHull != null && character.CurrentHull != safeHull) { RemoveSubObjective(ref goToObjective); TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(safeHull, character, objectiveManager), @@ -132,10 +145,13 @@ namespace Barotrauma { // We can start applying treatment if (character != targetCharacter && character.SelectedCharacter != targetCharacter) - { - character.Speak(TextManager.GetWithVariables("DialogFoundWoundedTarget", new string[2] { "[targetname]", "[roomname]" }, - new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }), - null, 1.0f, "foundwoundedtarget" + targetCharacter.Name, 60.0f); + { + if (targetCharacter.CurrentHull.DisplayName != null) + { + character.Speak(TextManager.GetWithVariables("DialogFoundWoundedTarget", new string[2] { "[targetname]", "[roomname]" }, + new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }), + null, 1.0f, "foundwoundedtarget" + targetCharacter.Name, 60.0f); + } character.SelectCharacter(targetCharacter); } @@ -151,7 +167,7 @@ namespace Barotrauma if (!targetCharacter.IsPlayer) { // If the target is a bot, don't let it move - targetCharacter.AIController.SteeringManager.Reset(); + targetCharacter.AIController?.SteeringManager.Reset(); } if (treatmentTimer > 0.0f) { @@ -278,6 +294,11 @@ namespace Barotrauma public override float GetPriority() { + if (!IsAllowed) + { + Priority = 0; + return Priority; + } if (targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead) { Priority = 0; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs index d3d9adfed..4ea96940d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRescueAll.cs @@ -73,6 +73,7 @@ namespace Barotrauma public static float GetVitalityFactor(Character character) { float vitality = character.HealthPercentage - character.Bleeding - character.Bloodloss + Math.Min(character.Oxygen, 0); + vitality -= character.CharacterHealth.GetAfflictionStrength("paralysis"); return Math.Clamp(vitality, 0, 100); } @@ -105,7 +106,11 @@ namespace Barotrauma if (target.Submarine == null || character.Submarine == null) { return false; } if (target.Submarine.TeamID != character.Submarine.TeamID) { return false; } if (target.CurrentHull == null) { return false; } - if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, true)) { return false; } + if (character.Submarine != null) + { + if (target.Submarine.Info.Type != character.Submarine.Info.Type) { return false; } + if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, true)) { return false; } + } if (!target.IsPlayer && HumanAIController.IsActive(target) && target.AIController is HumanAIController targetAI) { // Ignore all concious targets that are currently fighting, fleeing or treating characters diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs index 16ba0c9ea..d5ad028e1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Order.cs @@ -1,4 +1,5 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -13,8 +14,7 @@ namespace Barotrauma Movement, Power, Maintenance, - Operate, - Undefined + Operate } class Order @@ -31,11 +31,7 @@ namespace Barotrauma return order; } - public Order Prefab - { - get; - private set; - } + public Order Prefab { get; private set; } public readonly string Name; @@ -55,7 +51,7 @@ namespace Barotrauma { return color.Value; } - else if (OrderCategoryIcons.TryGetValue(Category, out Tuple sprite)) + else if (Category.HasValue && OrderCategoryIcons.TryGetValue((OrderCategory)Category, out Tuple sprite)) { return sprite.Item2; } @@ -83,7 +79,8 @@ namespace Barotrauma public Character OrderGiver; - public readonly OrderCategory Category; + private readonly OrderCategory? category; + public OrderCategory? Category => category; //legacy support public readonly string[] AppropriateJobs; @@ -93,6 +90,25 @@ namespace Barotrauma public readonly Dictionary OptionSprites; public readonly float Weight; + public readonly bool MustSetTarget; + public readonly string AppropriateSkill; + + public bool HasOptions + { + get + { + if (IsPrefab) + { + return MustSetTarget || Options.Length > 1; + } + else + { + return Prefab.MustSetTarget || Prefab.Options.Length > 1; + } + } + } + public bool IsPrefab { get; private set; } + public readonly bool MustManuallyAssign; static Order() { @@ -197,8 +213,11 @@ namespace Barotrauma TargetAllCharacters = orderElement.GetAttributeBool("targetallcharacters", false); AppropriateJobs = orderElement.GetAttributeStringArray("appropriatejobs", new string[0]); Options = orderElement.GetAttributeStringArray("options", new string[0]); - Category = (OrderCategory)Enum.Parse(typeof(OrderCategory), orderElement.GetAttributeString("category", "undefined"), true); + var category = orderElement.GetAttributeString("category", null); + if (!string.IsNullOrWhiteSpace(category)) { this.category = (OrderCategory)Enum.Parse(typeof(OrderCategory), category, true); } Weight = orderElement.GetAttributeFloat(0.0f, "weight"); + MustSetTarget = orderElement.GetAttributeBool("mustsettarget", false); + AppropriateSkill = orderElement.GetAttributeString("appropriateskill", null); string translatedOptionNames = TextManager.Get("OrderOptions." + Identifier, true); if (translatedOptionNames == null) @@ -241,6 +260,9 @@ namespace Barotrauma } } } + + IsPrefab = true; + MustManuallyAssign = orderElement.GetAttributeBool("mustmanuallyassign", false); } /// @@ -253,6 +275,7 @@ namespace Barotrauma Name = prefab.Name; Identifier = prefab.Identifier; ItemComponentType = prefab.ItemComponentType; + ItemIdentifiers = prefab.ItemIdentifiers; Options = prefab.Options; SymbolSprite = prefab.SymbolSprite; Color = prefab.Color; @@ -261,22 +284,35 @@ namespace Barotrauma AppropriateJobs = prefab.AppropriateJobs; FadeOutTime = prefab.FadeOutTime; Weight = prefab.Weight; - Category = prefab.Category; - OrderGiver = orderGiver; + MustSetTarget = prefab.MustSetTarget; + AppropriateSkill = prefab.AppropriateSkill; + category = prefab.Category; + MustManuallyAssign = prefab.MustManuallyAssign; + OrderGiver = orderGiver; TargetEntity = targetEntity; if (targetItem != null) { - if (UseController) - { - //try finding the controller with the simpler non-recursive method first - ConnectedController = - targetItem.Item.GetConnectedComponents().FirstOrDefault() ?? - targetItem.Item.GetConnectedComponents(recursive: true).FirstOrDefault(); - } + if (UseController) { ConnectedController = FindController(targetItem); } TargetEntity = targetItem.Item; TargetItemComponent = targetItem; } + + IsPrefab = false; + } + + private Controller FindController(ItemComponent targetComponent) + { + if (targetComponent?.Item == null) { return null; } + //try finding the controller with the simpler non-recursive method first + return targetComponent.Item.GetConnectedComponents().FirstOrDefault() ?? + targetComponent.Item.GetConnectedComponents(recursive: true).FirstOrDefault(); + } + + private bool TryFindController(ItemComponent targetComponent, out Controller controller) + { + controller = FindController(targetComponent); + return controller != null; } public bool HasAppropriateJob(Character character) @@ -291,7 +327,7 @@ namespace Barotrauma } for (int i = 0; i < AppropriateJobs.Length; i++) { - if (character.Info.Job.Prefab.Identifier.ToLowerInvariant() == AppropriateJobs[i].ToLowerInvariant()) { return true; } + if (character.Info.Job.Prefab.Identifier.Equals(AppropriateJobs[i], StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } @@ -310,5 +346,40 @@ namespace Barotrauma return msg; } + + public List GetMatchingItems(Submarine submarine, bool mustBelongToPlayerSub) + { + List matchingItems = new List(); + if (submarine == null) { return matchingItems; } + if (ItemComponentType != null || ItemIdentifiers.Length > 0) + { + matchingItems = ItemIdentifiers.Length > 0 ? + Item.ItemList.FindAll(it => ItemIdentifiers.Contains(it.Prefab.Identifier) || it.HasTag(ItemIdentifiers)) : + Item.ItemList.FindAll(it => it.Components.Any(ic => ic.GetType() == ItemComponentType)); + if (mustBelongToPlayerSub) + { + matchingItems.RemoveAll(it => it.Submarine?.Info != null && it.Submarine.Info.Type != SubmarineInfo.SubmarineType.Player); + matchingItems.RemoveAll(it => it.Submarine != submarine && !submarine.DockedTo.Contains(it.Submarine)); + } + else + { + matchingItems.RemoveAll(it => it.Submarine != submarine); + } + matchingItems.RemoveAll(it => it.NonInteractable); + if (UseController) + { + matchingItems.RemoveAll(i => i.Components.None(c => c.GetType() == ItemComponentType && TryFindController(c, out _))); + } + } + return matchingItems; + } + + public List GetMatchingItems(bool mustBelongToPlayerSub) + { + Submarine submarine = Character.Controlled != null && Character.Controlled.TeamID == Character.TeamType.Team2 && Submarine.MainSubs.Length > 1 ? + Submarine.MainSubs[1] : + Submarine.MainSub; + return GetMatchingItems(submarine, mustBelongToPlayerSub); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs new file mode 100644 index 000000000..fe06aa3a4 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAI.cs @@ -0,0 +1,377 @@ +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using Barotrauma.Networking; +using System.Linq; +using System; + +namespace Barotrauma +{ + partial class WreckAI : IServerSerializable + { + public Submarine Wreck { get; private set; } + + public bool IsAlive { get; private set; } + + private readonly List allItems; + private readonly List thalamusItems; + private readonly List thalamusStructures; + private readonly List turrets = new List(); + private readonly List wayPoints = new List(); + private readonly List hulls = new List(); + private readonly List spawnOrgans = new List(); + private readonly Item brain; + + private bool initialCellsSpawned; + + public readonly WreckAIConfig Config; + + private bool IsClient => GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; + + private bool IsThalamus(MapEntityPrefab entityPrefab) => IsThalamus(entityPrefab, Config.Entity); + + private static IEnumerable GetThalamusEntities(Submarine wreck, string tag) where T : MapEntity => GetThalamusEntities(wreck, tag).Where(e => e is T).Select(e => e as T); + + private static IEnumerable GetThalamusEntities(Submarine wreck, string tag) => MapEntity.mapEntityList.Where(e => e.Submarine == wreck && e.prefab != null && IsThalamus(e.prefab, tag)); + + private static bool IsThalamus(MapEntityPrefab entityPrefab, string tag) => entityPrefab.Category == MapEntityCategory.Thalamus || entityPrefab.Tags.Contains(tag); + + public WreckAI(Submarine wreck) + { + Wreck = wreck; + Config = WreckAIConfig.GetRandom(); + if (Config == null) + { + DebugConsole.ThrowError("WreckAI: No wreck AI config found!"); + return; + } + var thalamusPrefabs = ItemPrefab.Prefabs.Where(p => IsThalamus(p)); + var brainPrefab = thalamusPrefabs.GetRandom(i => i.Tags.Contains(Config.Brain), Rand.RandSync.Server); + if (brainPrefab == null) + { + DebugConsole.ThrowError($"WreckAI: Could not find any brain prefab with the tag {Config.Brain}! Cannot continue. Failed to create wreck AI."); + return; + } + allItems = Wreck.GetItems(false); + thalamusItems = allItems.FindAll(i => IsThalamus(i.prefab)); + var hulls = Wreck.GetHulls(false); + brain = new Item(brainPrefab, Vector2.Zero, Wreck); + thalamusItems.Add(brain); + Vector2 negativeMargin = new Vector2(40, 20); + Vector2 minSize = brain.Rect.Size.ToVector2() - negativeMargin; + Vector2 maxSize = new Vector2(brain.Rect.Width * 3, brain.Rect.Height * 3); + // First try to get a room that is not too big and not in the edges of the sub. + // Also try not to create the brain in a room that already have carrier items inside. + // Ignore hulls that have any linked hulls to keep the calculations simple. + // Shrink the horizontal axis so that the brain is not placed in the left or right side, where we often have curved walls. + // Also ignore hulls that have open gaps, because we'll want the room to be full of water. The room will be filled with water when the brain is inserted in the room. + Rectangle shrinkedBounds = ToolBox.GetWorldBounds(Wreck.WorldPosition.ToPoint(), new Point(Wreck.Borders.Width - 500, Wreck.Borders.Height)); + bool BaseCondition(Hull h) => h.RectWidth > minSize.X && h.RectHeight > minSize.Y && h.GetLinkedEntities().None() && h.ConnectedGaps.None(g => g.Open > 0); + bool IsNotTooBig(Hull h) => h.RectWidth < maxSize.X && h.RectHeight < maxSize.Y; + bool IsNotInFringes(Hull h) => shrinkedBounds.ContainsWorld(h.WorldRect); + bool DoesNotContainOtherItems(Hull h) => thalamusItems.None(i => i.CurrentHull == h); + Hull brainHull = hulls.GetRandom(h => BaseCondition(h) && IsNotTooBig(h) && IsNotInFringes(h) && DoesNotContainOtherItems(h), Rand.RandSync.Server); + if (brainHull == null) + { + brainHull = hulls.GetRandom(h => BaseCondition(h) && IsNotInFringes(h) && DoesNotContainOtherItems(h), Rand.RandSync.Server); + } + if (brainHull == null) + { + brainHull = hulls.GetRandom(h => BaseCondition(h) && (IsNotInFringes(h) || DoesNotContainOtherItems(h)), Rand.RandSync.Server); + } + if (brainHull == null) + { + brainHull = hulls.GetRandom(BaseCondition, Rand.RandSync.Server); + } + var thalamusStructurePrefabs = StructurePrefab.Prefabs.Where(p => IsThalamus(p)); + if (brainHull == null) { return; } + brainHull.WaterVolume = brainHull.Volume; + brain.SetTransform(brainHull.SimPosition, rotation: 0, findNewHull: false); + brain.CurrentHull = brainHull; + var backgroundPrefab = thalamusStructurePrefabs.GetRandom(i => i.Tags.Contains(Config.BrainRoomBackground), Rand.RandSync.Server); + if (backgroundPrefab != null) + { + new Structure(brainHull.Rect, backgroundPrefab, Wreck); + } + var horizontalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomHorizontalWall), Rand.RandSync.Server); + if (horizontalWallPrefab != null) + { + int height = (int)horizontalWallPrefab.Size.Y; + int halfHeight = height / 2; + int quarterHeight = halfHeight / 2; + new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, Wreck); + new Structure(new Rectangle(brainHull.Rect.Left, brainHull.Rect.Top - brainHull.Rect.Height + halfHeight + quarterHeight, brainHull.Rect.Width, height), horizontalWallPrefab, Wreck); + } + var verticalWallPrefab = thalamusStructurePrefabs.GetRandom(p => p.Tags.Contains(Config.BrainRoomVerticalWall), Rand.RandSync.Server); + if (verticalWallPrefab != null) + { + int width = (int)verticalWallPrefab.Size.X; + int halfWidth = width / 2; + int quarterWidth = halfWidth / 2; + new Structure(new Rectangle(brainHull.Rect.Left - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, Wreck); + new Structure(new Rectangle(brainHull.Rect.Right - halfWidth - quarterWidth, brainHull.Rect.Top, width, brainHull.Rect.Height), verticalWallPrefab, Wreck); + } + foreach (Item item in allItems) + { + if (thalamusItems.Contains(item)) + { + // Ensure that thalamus items are visible + item.HiddenInGame = false; + } + else + { + // Load regular turrets + var turret = item.GetComponent(); + if (turret != null) + { + foreach (var linkedItem in item.GetLinkedEntities()) + { + var container = linkedItem.GetComponent(); + if (container == null) { continue; } + for (int i = 0; i < container.Inventory.Capacity; i++) + { + if (container.Inventory.Items[i] != null) { continue; } + if (MapEntityPrefab.List.GetRandom(e => e is ItemPrefab i && container.CanBeContained(i) && + Config.ForbiddenAmmunition.None(id => id.Equals(i.Identifier, StringComparison.OrdinalIgnoreCase)), Rand.RandSync.Server) is ItemPrefab ammoPrefab) + { + Item ammo = new Item(ammoPrefab, container.Item.WorldPosition, Wreck); + if (!container.Inventory.TryPutItem(ammo, i, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: false)) + { + item.Remove(); + } + } + } + } + } + } + } + foreach (var item in allItems) + { + var turret = item.GetComponent(); + if (turret != null) + { + turrets.Add(turret); + } + if (item.HasTag(Config.Spawner)) + { + if (!spawnOrgans.Contains(item)) + { + spawnOrgans.Add(item); + } + } + } + wayPoints.AddRange(Wreck.GetWaypoints(false)); + hulls.AddRange(Wreck.GetHulls(false)); + IsAlive = true; + thalamusStructures = GetThalamusEntities(Wreck, Config.Entity).ToList(); + } + + private readonly List destroyedOrgans = new List(); + public void Update(float deltaTime) + { + if (!IsAlive) { return; } + if (Wreck == null || Wreck.Removed) + { + Remove(); + return; + } + if (brain == null || brain.Removed || brain.Condition <= 0) + { + Kill(); + return; + } + destroyedOrgans.Clear(); + foreach (var organ in spawnOrgans) + { + if (organ.Condition <= 0) + { + destroyedOrgans.Add(organ); + } + } + destroyedOrgans.ForEach(o => spawnOrgans.Remove(o)); + bool someoneNearby = false; + float minDist = Sonar.DefaultSonarRange * 2.0f; + foreach (Submarine submarine in Submarine.Loaded) + { + if (submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; } + if (Vector2.DistanceSquared(submarine.WorldPosition, Wreck.WorldPosition) < minDist * minDist) + { + someoneNearby = true; + break; + } + } + foreach (Character c in Character.CharacterList) + { + if (c != Character.Controlled && !c.IsRemotePlayer) { continue; } + if (Vector2.DistanceSquared(c.WorldPosition, Wreck.WorldPosition) < minDist * minDist) + { + someoneNearby = true; + break; + } + } + if (!someoneNearby) { return; } + OperateTurrets(deltaTime); + if (!IsClient) + { + if (!initialCellsSpawned) { SpawnInitialCells(); } + UpdateReinforcements(deltaTime); + } + } + + private void SpawnInitialCells() + { + int brainRoomCells = Rand.Range(MinCellsPerBrainRoom, MaxCellsPerRoom); + if (brain.CurrentHull?.WaterPercentage >= MinWaterLevel) + { + for (int i = 0; i < brainRoomCells; i++) + { + if (!TrySpawnCell(out _, brain.CurrentHull)) { break; } + } + } + int cellsInside = Rand.Range(MinCellsInside, MaxCellsInside); + for (int i = 0; i < cellsInside; i++) + { + if (!TrySpawnCell(out _)) { break; } + } + int cellsOutside = Rand.Range(MinCellsOutside, MaxCellsOutside); + // If we failed to spawn some of the cells in the brainroom/inside, spawn some extra cells outside. + cellsOutside = Math.Clamp(cellsOutside + brainRoomCells + cellsInside - protectiveCells.Count, cellsOutside, MaxCellsOutside); + for (int i = 0; i < cellsOutside; i++) + { + ISpatialEntity targetEntity = wayPoints.GetRandom(wp => wp.CurrentHull == null); + if (targetEntity == null) { break; } + if (!TrySpawnCell(out _, targetEntity)) { break; } + } + initialCellsSpawned = true; + } + + private void Kill() + { + thalamusItems.ForEach(i => i.Condition = 0); + foreach (var turret in turrets) + { + // Snap all tendons + foreach (Item item in turret.ActiveProjectiles) + { + if (item.GetComponent()?.IsStuckToTarget ?? false) + { + item.Condition = 0; + } + } + } + FadeOutColors(); + protectiveCells.ForEach(c => c.OnDeath -= OnCellDeath); + if (!IsClient) + { + if (Config != null) + { + if (Config.KillAgentsWhenEntityDies) + { + protectiveCells.ForEach(c => c.Kill(CauseOfDeathType.Unknown, null, isNetworkMessage: true)); + } + } + } + protectiveCells.Clear(); + IsAlive = false; + } + + partial void FadeOutColors(); + + public void Remove() + { + Kill(); + RemoveThalamusItems(Wreck); + thalamusItems?.Clear(); + thalamusStructures?.Clear(); + } + + public static void RemoveThalamusItems(Submarine wreck) + { + foreach (var wreckAiConfig in WreckAIConfig.List) + { + GetThalamusEntities(wreck, wreckAiConfig.Entity).ForEachMod(e => e.Remove()); + } + } + + // The client doesn't use these, so we don't have to sync them. + private readonly List protectiveCells = new List(); + // Intentionally contains duplicates. + private readonly List populatedHulls = new List(); + private float cellSpawnTimer; + + private float CellSpawnTime => Config.AgentSpawnDelay; + private float CellSpawnRandomFactor => Config.AgentSpawnDelayRandomFactor; + private int MinCellsPerBrainRoom => Config.MinAgentsPerBrainRoom; + private int MaxCellsPerRoom => Config.MaxAgentsPerRoom; + private int MinCellsOutside => Config.MinAgentsOutside; + private int MaxCellsOutside => Config.MaxAgentsOutside; + private int MinCellsInside => Config.MinAgentsInside; + private int MaxCellsInside => Config.MaxAgentsInside; + private int MaxCellCount => Config.MaxAgentCount; + private float MinWaterLevel => Config.MinWaterLevel; + + void UpdateReinforcements(float deltaTime) + { + if (protectiveCells.Count >= MaxCellCount || spawnOrgans.Count == 0) { return; } + cellSpawnTimer -= deltaTime; + if (cellSpawnTimer < 0) + { + TrySpawnCell(out _, spawnOrgans.GetRandom()); + cellSpawnTimer = CellSpawnTime * Rand.Range(CellSpawnRandomFactor, 1 + CellSpawnRandomFactor); + } + } + + bool TrySpawnCell(out Character cell, ISpatialEntity targetEntity = null) + { + cell = null; + if (protectiveCells.Count >= MaxCellCount) { return false; } + if (targetEntity == null) + { + targetEntity = + wayPoints.GetRandom(wp => wp.CurrentHull != null && populatedHulls.Count(h => h == wp.CurrentHull) < MaxCellsPerRoom && wp.CurrentHull.WaterPercentage >= MinWaterLevel) ?? + hulls.GetRandom(h => populatedHulls.Count(h2 => h2 == h) < MaxCellsPerRoom && h.WaterPercentage >= MinWaterLevel) as ISpatialEntity; + } + if (targetEntity == null) { return false; } + if (targetEntity is Hull h) + { + populatedHulls.Add(h); + } + else if (targetEntity is WayPoint wp && wp.CurrentHull != null) + { + populatedHulls.Add(wp.CurrentHull); + } + // Don't add items in the list, because we want to be able to ignore the restrictions for spawner organs. + cell = Character.Create(Config.DefensiveAgent, targetEntity.WorldPosition, ToolBox.RandomSeed(8), hasAi: true, createNetworkEvent: true); + protectiveCells.Add(cell); + cell.OnDeath += OnCellDeath; + cellSpawnTimer = CellSpawnTime * Rand.Range(CellSpawnRandomFactor, 1 + CellSpawnRandomFactor); + return true; + } + + void OperateTurrets(float deltaTime) + { + foreach (var turret in turrets) + { + // Never target other creatures than humans with the turrets. + turret.ThalamusOperate(this, deltaTime, + !turret.Item.HasTag("ignorecharacters"), + targetOtherCreatures: false, + !turret.Item.HasTag("ignoresubmarines"), + turret.Item.HasTag("ignoreaimdelay")); + } + } + + void OnCellDeath(Character character, CauseOfDeath causeOfDeath) + { + protectiveCells.Remove(character); + } + +#if SERVER + public void ServerWrite(IWriteMessage msg, Client client, object[] extraData = null) + { + msg.Write(IsAlive); + } +#endif + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs new file mode 100644 index 000000000..87515b535 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Wreck/WreckAIConfig.cs @@ -0,0 +1,127 @@ +using Barotrauma.Extensions; +using Microsoft.Xna.Framework; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + class WreckAIConfig : ISerializableEntity + { + public string Name => "Wreck AI Config"; + + public Dictionary SerializableProperties { get; private set; } + + [Serialize("", false)] + public string Entity { get; private set; } + + [Serialize("", false)] + public string DefensiveAgent { get; private set; } + + [Serialize("", false)] + public string Brain { get; private set; } + + [Serialize("", false)] + public string Spawner { get; private set; } + + [Serialize("", false)] + public string BrainRoomBackground { get; private set; } + + [Serialize("", false)] + public string BrainRoomVerticalWall { get; private set; } + + [Serialize("", false)] + public string BrainRoomHorizontalWall { get; private set; } + + [Serialize(60f, false)] + public float AgentSpawnDelay { get; private set; } + + [Serialize(0.5f, false)] + public float AgentSpawnDelayRandomFactor { get; private set; } + + [Serialize(0, false)] + public int MinAgentsPerBrainRoom { get; private set; } + + [Serialize(3, false)] + public int MaxAgentsPerRoom { get; private set; } + + [Serialize(2, false)] + public int MinAgentsOutside { get; private set; } + + [Serialize(5, false)] + public int MaxAgentsOutside { get; private set; } + + [Serialize(3, false)] + public int MinAgentsInside { get; private set; } + + [Serialize(10, false)] + public int MaxAgentsInside { get; private set; } + + [Serialize(15, false)] + public int MaxAgentCount { get; private set; } + + [Serialize(100f, false)] + public float MinWaterLevel { get; private set; } + + [Serialize(true, false)] + public bool KillAgentsWhenEntityDies { get; private set; } + + [Serialize(1f, false)] + public float DeadEntityColorMultiplier { get; private set; } + + [Serialize(1f, false)] + public float DeadEntityColorFadeOutTime { get; private set; } + + public readonly string[] ForbiddenAmmunition; + + public static List List + { + get + { + if (paramsList == null) + { + LoadAll(); + } + return paramsList; + } + } + + private static List paramsList; + + public static WreckAIConfig GetRandom() => List.GetRandom(Rand.RandSync.Server); + + public WreckAIConfig(XElement element) + { + SerializableProperties = SerializableProperty.DeserializeProperties(this, element); + ForbiddenAmmunition = XMLExtensions.GetAttributeStringArray(element, "ForbiddenAmmunition", new string[0], convertToLowerInvariant: true); + } + + public static void LoadAll() + { + paramsList = new List(); + var files = GameMain.Instance.GetFilesOfType(ContentType.WreckAIConfig); + if (files.None()) + { + DebugConsole.ThrowError("Cannot find any Wreck AI config!"); + return; + } + foreach (ContentFile file in files) + { + XDocument doc = XMLExtensions.TryLoadXml(file.Path); + if (doc == null) { continue; } + var mainElement = doc.Root; + if (mainElement.IsOverride()) + { + mainElement = doc.Root.FirstElement(); + paramsList.Clear(); + DebugConsole.NewMessage($"Overriding the wreck ai config with '{file.Path}'", Color.Yellow); + } + else if (paramsList.Any()) + { + DebugConsole.NewMessage($"Adding additional wreck ai config from file '{file.Path}'"); + } + paramsList.Add(new WreckAIConfig(mainElement)); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs index 41812f4e9..e8a4e4ff4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AICharacter.cs @@ -46,8 +46,12 @@ namespace Barotrauma { base.Update(deltaTime, cam); - if (!Enabled) return; + if (!Enabled) { return; } + if (IsDead || Vitality <= 0.0f || Stun > 0.0f || IsIncapacitated) { return; } + //don't enable simple physics on dead/incapacitated characters + //the ragdoll controls the movement of incapacitated characters instead of the collider, + //but in simple physics mode the ragdoll would get disabled, causing the character to not move at all if (!IsRemotePlayer) { float characterDist = float.MaxValue; @@ -70,10 +74,9 @@ namespace Barotrauma } } - if (IsDead || Vitality <= 0.0f || IsUnconscious || Stun > 0.0f) return; - if (!aiController.Enabled) return; - if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) return; - if (Controlled == this) return; + if (!aiController.Enabled) { return; } + if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; } + if (Controlled == this) { return; } if (!IsRemotePlayer) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index 296aff27d..f1e43f569 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -441,7 +441,7 @@ namespace Barotrauma for (int i = -1; i < 2; i += 2) { Vector2 footPos = GetColliderBottom(); - footPos = new Vector2(waist.SimPosition.X + Math.Sign(StepSize.Value.X * i) * Dir * 0.3f, footPos.Y - 0.1f * RagdollParams.JointScale); + footPos = new Vector2(waist.SimPosition.X + Math.Sign(WalkParams.StepSize.X * i) * Dir * 0.3f, footPos.Y - 0.1f * RagdollParams.JointScale); var foot = i == -1 ? rightFoot : leftFoot; MoveLimb(foot, footPos, Math.Abs(foot.SimPosition.X - footPos.X) * 100.0f, true); } @@ -481,7 +481,7 @@ namespace Barotrauma swimmingStateLockTimer -= deltaTime; - if (forceStanding) + if (forceStanding || character.AnimController.AnimationTestPose) { swimming = false; } @@ -1410,7 +1410,7 @@ namespace Barotrauma SteamAchievementManager.OnCharacterRevived(target, character); lastReviveTime = (float)Timing.TotalTime; #if SERVER - GameMain.Server?.KarmaManager?.OnCharacterHealthChanged(target, character, damage: Math.Min(prevVitality - target.Vitality, 0.0f)); + GameMain.Server?.KarmaManager?.OnCharacterHealthChanged(target, character, damage: Math.Min(prevVitality - target.Vitality, 0.0f), stun: 0.0f); #endif //reset attacker, we don't want the character to start attacking us //because we caused a bit of damage to them during CPR @@ -1540,6 +1540,11 @@ namespace Barotrauma { sourceSimPos -= character.SelectedCharacter.Submarine.SimPosition; } + else if (character.Submarine != null && character.SelectedCharacter.Submarine != null && character.Submarine != character.SelectedCharacter.Submarine) + { + targetSimPos += character.SelectedCharacter.Submarine.SimPosition; + targetSimPos -= character.Submarine.SimPosition; + } var body = Submarine.CheckVisibility(sourceSimPos, targetSimPos, ignoreSubs: true); if (body != null) { @@ -1553,8 +1558,8 @@ namespace Barotrauma Vector2 diff = ConvertUnits.ToSimUnits(targetLimb.WorldPosition - pullLimb.WorldPosition); - Vector2 targetAnchor = targetLimb.SimPosition; - float targetForce = 0.0f; + Vector2 targetAnchor; + float targetForce; pullLimb.PullJointEnabled = true; if (targetLimb.type == LimbType.Torso || targetLimb == target.AnimController.MainLimb) { @@ -1624,6 +1629,7 @@ namespace Barotrauma if (!target.AllowInput) { + target.AnimController.Stairs = Stairs; target.AnimController.IgnorePlatforms = IgnorePlatforms; target.AnimController.TargetMovement = TargetMovement; } @@ -1652,7 +1658,10 @@ namespace Barotrauma //TODO: refactor this method, it's way too convoluted public override void HoldItem(float deltaTime, Item item, Vector2[] handlePos, Vector2 holdPos, Vector2 aimPos, bool aim, float holdAngle, float itemAngleRelativeToHoldAngle = 0.0f) { - if (character.IsUnconscious || character.Stun > 0.0f) aim = false; + if (character.Stun > 0.0f || character.IsIncapacitated) + { + aim = false; + } //calculate the handle positions Matrix itemTransfrom = Matrix.CreateRotationZ(item.body.Rotation); @@ -1676,7 +1685,7 @@ namespace Barotrauma Holdable holdable = item.GetComponent(); - if (!isClimbing && !usingController && character.Stun <= 0.0f && aim && itemPos != Vector2.Zero) + if (!isClimbing && !usingController && character.Stun <= 0.0f && aim && itemPos != Vector2.Zero && !character.IsIncapacitated) { Vector2 mousePos = ConvertUnits.ToSimUnits(character.SmoothedCursorPosition); @@ -1763,7 +1772,7 @@ namespace Barotrauma if (holdable.Pusher != null) { - if (character.IsUnconscious || character.Stun > 0.0f) + if (character.Stun > 0.0f || character.IsIncapacitated) { holdable.Pusher.Enabled = false; } @@ -1778,7 +1787,7 @@ namespace Barotrauma else { holdable.Pusher.TargetPosition = currItemPos; - holdable.Pusher.TargetRotation = character.IsUnconscious || character.Stun > 0.0f ? itemAngle : holdAngle * Dir; + holdable.Pusher.TargetRotation = holdAngle * Dir; holdable.Pusher.MoveToTargetPosition(true); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 519343441..154402779 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -55,7 +55,11 @@ namespace Barotrauma { if (limbs == null) { - DebugConsole.ThrowError("Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this)); + string errorMsg = "Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this); +#if DEBUG || UNSTABLE + errorMsg += '\n' + Environment.StackTrace; +#endif + DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce( "Ragdoll.Limbs:AccessRemoved", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, @@ -80,7 +84,7 @@ namespace Barotrauma frozen = value; Collider.FarseerBody.LinearDamping = frozen ? (1.5f / (float)Timing.Step) : 0.0f; - Collider.FarseerBody.AngularDamping = frozen ? (1.5f / (float)Timing.Step) : 0.0f; + Collider.FarseerBody.AngularDamping = frozen ? (1.5f / (float)Timing.Step) : PhysicsBody.DefaultAngularDamping; Collider.FarseerBody.IgnoreGravity = frozen; //Collider.PhysEnabled = !frozen; @@ -97,8 +101,6 @@ namespace Barotrauma protected float strongestImpact; - protected double onFloorTimer; - private float splashSoundTimer; //the movement speed of the ragdoll @@ -635,10 +637,7 @@ namespace Barotrauma Vector2 colliderBottom = GetColliderBottom(); if (structure.IsPlatform) { - if (IgnorePlatforms) { return false; } - - //the collision is ignored if the lowest limb is under the platform - //if (lowestLimb==null || lowestLimb.Position.Y < structure.Rect.Y) return false; + if (IgnorePlatforms || currentHull == null) { return false; } if (colliderBottom.Y < ConvertUnits.ToSimUnits(structure.Rect.Y - 5)) { return false; } if (f1.Body.Position.Y < ConvertUnits.ToSimUnits(structure.Rect.Y - 5)) { return false; } @@ -732,14 +731,20 @@ namespace Barotrauma limbJoint.IsSevered = true; limbJoint.Enabled = false; + Vector2 limbDiff = limbJoint.LimbA.SimPosition - limbJoint.LimbB.SimPosition; + if (limbDiff.LengthSquared() < 0.0001f) { limbDiff = Rand.Vector(1.0f); } + limbDiff = Vector2.Normalize(limbDiff); + float mass = limbJoint.BodyA.Mass + limbJoint.BodyB.Mass; + limbJoint.LimbA.body.ApplyLinearImpulse(limbDiff * mass, (limbJoint.LimbA.SimPosition + limbJoint.LimbB.SimPosition) / 2.0f); + limbJoint.LimbB.body.ApplyLinearImpulse(-limbDiff * mass, (limbJoint.LimbA.SimPosition + limbJoint.LimbB.SimPosition) / 2.0f); + List connectedLimbs = new List(); List checkedJoints = new List(); GetConnectedLimbs(connectedLimbs, checkedJoints, MainLimb); foreach (Limb limb in Limbs) { - if (connectedLimbs.Contains(limb)) continue; - + if (connectedLimbs.Contains(limb)) { continue; } limb.IsSevered = true; } @@ -962,8 +967,8 @@ namespace Barotrauma else { if (character.Position.X < gap.Rect.X || character.Position.X > gap.Rect.Right) continue; - if (Math.Sign((gap.Rect.Y - gap.Rect.Height / 2) - (currentHull.Rect.Center.Y - currentHull.Rect.Height / 2)) != - Math.Sign(character.Position.Y - (currentHull.Rect.Center.Y - currentHull.Rect.Height / 2))) + if (Math.Sign((gap.Rect.Y - gap.Rect.Height / 2) - (currentHull.Rect.Y - currentHull.Rect.Height / 2)) != + Math.Sign(character.Position.Y - (currentHull.Rect.Y - currentHull.Rect.Height / 2))) { continue; } @@ -1224,6 +1229,8 @@ namespace Barotrauma private void CheckBodyInRest(float deltaTime) { + if (SimplePhysicsEnabled) { return; } + if (InWater || Collider.LinearVelocity.LengthSquared() > 0.01f || character.SelectedBy != null || !character.IsDead) { bodyInRestTimer = 0.0f; @@ -1633,7 +1640,7 @@ namespace Barotrauma { float offset = 0.0f; - if (!character.IsUnconscious && !character.IsDead && character.Stun <= 0.0f) + if (!character.IsDead && character.Stun <= 0.0f && !character.IsIncapacitated) { offset = -ColliderHeightFromFloor; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index cb323fc2a..6cc8bb8ff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -260,7 +260,7 @@ namespace Barotrauma return totalDamage; } - public Attack(float damage, float bleedingDamage, float burnDamage, float structureDamage, float range = 0.0f) + public Attack(float damage, float bleedingDamage, float burnDamage, float structureDamage, float itemDamage, float range = 0.0f) { if (damage > 0.0f) Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(damage), null); if (bleedingDamage > 0.0f) Afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamage), null); @@ -269,6 +269,7 @@ namespace Barotrauma Range = range; DamageRange = range; StructureDamage = structureDamage; + ItemDamage = itemDamage; } public Attack(XElement element, string parentDebugName) @@ -298,7 +299,7 @@ namespace Barotrauma { DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - define afflictions using identifiers instead of names."); string afflictionName = subElement.GetAttributeString("name", "").ToLowerInvariant(); - afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Name.ToLowerInvariant() == afflictionName); + afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Name.Equals(afflictionName, System.StringComparison.OrdinalIgnoreCase)); if (afflictionPrefab == null) { DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionName + "\" not found."); @@ -308,7 +309,7 @@ namespace Barotrauma else { string afflictionIdentifier = subElement.GetAttributeString("identifier", "").ToLowerInvariant(); - afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.ToLowerInvariant() == afflictionIdentifier); + afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.Equals(afflictionIdentifier, System.StringComparison.OrdinalIgnoreCase)); if (afflictionPrefab == null) { DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionIdentifier + "\" not found."); @@ -343,7 +344,7 @@ namespace Barotrauma AfflictionPrefab afflictionPrefab; Affliction affliction; string afflictionIdentifier = subElement.GetAttributeString("identifier", "").ToLowerInvariant(); - afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.ToLowerInvariant() == afflictionIdentifier); + afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier.Equals(afflictionIdentifier, System.StringComparison.OrdinalIgnoreCase)); if (afflictionPrefab != null) { float afflictionStrength = subElement.GetAttributeFloat(1.0f, "amount", "strength"); @@ -515,10 +516,10 @@ namespace Barotrauma public void SetCoolDown() { float randomFraction = CoolDown * CoolDownRandomFactor; - CurrentRandomCoolDown = MathHelper.Lerp(-randomFraction, randomFraction, Rand.Value(Rand.RandSync.Server)); + CurrentRandomCoolDown = MathHelper.Lerp(-randomFraction, randomFraction, Rand.Value()); CoolDownTimer = CoolDown + CurrentRandomCoolDown; randomFraction = SecondaryCoolDown * CoolDownRandomFactor; - SecondaryCoolDownTimer = SecondaryCoolDown + MathHelper.Lerp(-randomFraction, randomFraction, Rand.Value(Rand.RandSync.Server)); + SecondaryCoolDownTimer = SecondaryCoolDown + MathHelper.Lerp(-randomFraction, randomFraction, Rand.Value()); } public void ResetCoolDown() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index 87dff3a0a..d348011bf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -126,6 +126,14 @@ namespace Barotrauma set => Params.NeedsAir = value; } + public bool NeedsWater + { + get => Params.NeedsWater; + set => Params.NeedsWater = value; + } + + public bool NeedsOxygen => NeedsAir || NeedsWater && !AnimController.InWater; + public float Noise { get => Params.Noise; @@ -144,7 +152,28 @@ namespace Barotrauma private float attackCoolDown; - public Order CurrentOrder { get; private set; } + public Order CurrentOrder + { + get + { + return Info?.CurrentOrder; + } + private set + { + if (Info != null) { Info.CurrentOrder = value; } + } + } + public string CurrentOrderOption + { + get + { + return Info?.CurrentOrderOption; + } + private set + { + if (Info != null) { Info.CurrentOrderOption = value; } + } + } private readonly List statusEffects = new List(); private readonly List speedMultipliers = new List(); @@ -170,7 +199,7 @@ namespace Barotrauma if (turret != null) { viewTargetWorldPos = new Vector2( - targetItem.WorldRect.X + turret.TransformedBarrelPos.X, + targetItem.WorldRect.X + turret.TransformedBarrelPos.X, targetItem.WorldRect.Y - turret.TransformedBarrelPos.Y); } } @@ -255,7 +284,7 @@ namespace Barotrauma //text displayed when the character is highlighted if custom interact is set public string customInteractHUDText; private Action onCustomInteract; - + private float lockHandsTimer; public bool LockHands { @@ -271,22 +300,22 @@ namespace Barotrauma public bool AllowInput { - get { return !IsUnconscious && Stun <= 0.0f && !IsDead; } + get { return Stun <= 0.0f && !IsDead && !IsIncapacitated; } } public bool CanMove { get { - if (!AllowInput) { return false; } if (!AnimController.InWater && !AnimController.CanWalk) { return false; } + if (!AllowInput) { return false; } return true; } } public bool CanInteract { - get { return AllowInput && IsHumanoid && !LockHands && !Removed; } + get { return AllowInput && IsHumanoid && !LockHands && !Removed && !IsIncapacitated; } } public Vector2 CursorPosition @@ -372,12 +401,21 @@ namespace Barotrauma pressureProtection = MathHelper.Clamp(value, 0.0f, 100.0f); } } - + private float ragdollingLockTimer; public bool IsRagdolled; public bool IsForceRagdolled; public bool dontFollowCursor; + public bool IsIncapacitated + { + get + { + if (IsUnconscious) { return true; } + return CharacterHealth.Afflictions.Any(a => a.Prefab.AfflictionType == "paralysis" && a.Strength >= a.Prefab.MaxStrength); + } + } + public bool IsUnconscious { get { return CharacterHealth.IsUnconscious; } @@ -501,6 +539,8 @@ namespace Barotrauma public bool IsDead { get; private set; } + public bool EnableDespawn { get; set; } = true; + public CauseOfDeath CauseOfDeath { get; @@ -523,7 +563,7 @@ namespace Barotrauma { if (!canBeDragged) { return false; } if (Removed || !AnimController.Draggable) { return false; } - return IsDead || Stun > 0.0f || LockHands || IsUnconscious; + return IsDead || Stun > 0.0f || LockHands || IsIncapacitated; } set { canBeDragged = value; } } @@ -541,7 +581,7 @@ namespace Barotrauma } else { - return (IsDead || Stun > 0.0f || LockHands || IsUnconscious); + return (IsDead || Stun > 0.0f || LockHands || IsIncapacitated); } } set { canInventoryBeAccessed = value; } @@ -562,7 +602,9 @@ namespace Barotrauma { errorMsg += " AnimController.Collider == null"; } - +#if DEBUG || UNSTABLE + errorMsg += '\n' + Environment.StackTrace; +#endif DebugConsole.NewMessage(errorMsg, Color.Red); GameAnalyticsManager.AddErrorEventOnce( "Character.SimPosition:AccessRemoved", @@ -768,7 +810,7 @@ namespace Barotrauma var matchingAffliction = AfflictionPrefab.List .Where(p => p.AfflictionType == "huskinfection") .Select(p => p as AfflictionPrefabHusk) - .FirstOrDefault(p => p.TargetSpecies.Any(t => t.Equals(AfflictionHusk.GetNonHuskedSpeciesName(speciesName, p), StringComparison.InvariantCultureIgnoreCase))); + .FirstOrDefault(p => p.TargetSpecies.Any(t => t.Equals(AfflictionHusk.GetNonHuskedSpeciesName(speciesName, p), StringComparison.OrdinalIgnoreCase))); string nonHuskedSpeciesName = string.Empty; if (matchingAffliction == null) { @@ -1212,7 +1254,7 @@ namespace Barotrauma { attackCoolDown -= deltaTime; } - else if (IsKeyDown(InputType.Attack)) + else if (IsKeyDown(InputType.Attack) && (IsRemotePlayer || Controlled == this || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient))) { var currentContexts = GetAttackContexts(); var validLimbs = AnimController.Limbs.Where(l => !l.IsSevered && !l.IsStuck && l.attack != null && l.attack.IsValidContext(currentContexts)); @@ -1522,7 +1564,7 @@ namespace Barotrauma /// /// Finds the closest item seeking by identifiers or tags from the world. /// Ignores items that are outside or in another team's submarine or in a submarine that is not connected to this submarine. - /// Also ignores items that are taken by someone else. + /// Also ignores non-interactable items and items that are taken by someone else. /// The method is run in steps for performance reasons. So you'll have to provide the reference to the itemIndex. /// Returns false while running and true when done. /// @@ -1539,12 +1581,17 @@ namespace Barotrauma { itemIndex++; var item = Item.ItemList[itemIndex]; + if (item.NonInteractable) { continue; } if (ignoredItems != null && ignoredItems.Contains(item)) { continue; } if (item.Submarine == null) { continue; } if (item.Submarine.TeamID != TeamID) { continue; } - if (Submarine != null && !Submarine.IsEntityFoundOnThisSub(item, true)) { continue; } if (item.CurrentHull == null) { continue; } if (ignoreBroken && item.Condition <= 0) { continue; } + if (Submarine != null) + { + if (item.Submarine.Info.Type != Submarine.Info.Type) { continue; } + if (!Submarine.IsEntityFoundOnThisSub(item, true)) { continue; } + } if (customPredicate != null && !customPredicate(item)) { continue; } if (identifiers != null && identifiers.None(id => item.Prefab.Identifier == id || item.HasTag(id))) { continue; } if (ignoredContainerIdentifiers != null && item.Container != null) @@ -1785,13 +1832,17 @@ namespace Barotrauma if (findFocusedTimer <= 0.0f || Screen.Selected == GameMain.SubEditorScreen) { FocusedCharacter = CanInteract ? FindCharacterAtPosition(mouseSimPos) : null; + if (FocusedCharacter != null && !CanSeeCharacter(FocusedCharacter)) { FocusedCharacter = null; } float aimAssist = GameMain.Config.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f); if (SelectedItems.Any(it => it?.GetComponent()?.IsActive ?? false)) { //disable aim assist when rewiring to make it harder to accidentally select items when adding wire nodes aimAssist = 0.0f; } - focusedItem = CanInteract ? FindItemAtPosition(mouseSimPos, aimAssist) : null; + + var item = FindItemAtPosition(mouseSimPos, aimAssist); + + focusedItem = CanInteract ? item : null; findFocusedTimer = 0.05f; } } @@ -1925,7 +1976,7 @@ namespace Barotrauma if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { //disable AI characters that are far away from all clients and the host's character and not controlled by anyone - if (c.IsPlayer || c.IsBot) + if (c.IsPlayer || (c.IsBot && !c.IsDead)) { c.Enabled = true; } @@ -2094,12 +2145,17 @@ namespace Barotrauma UpdateControlled(deltaTime, cam); //Health effects - if (NeedsAir) { UpdateOxygen(deltaTime); } + if (NeedsOxygen) + { + UpdateOxygen(deltaTime); + } CharacterHealth.Update(deltaTime); - if (IsUnconscious) + if (IsIncapacitated) { - UpdateUnconscious(); + Stun = Math.Max(5.0f, Stun); + AnimController.ResetPullJoints(); + SelectedConstruction = null; return; } @@ -2138,11 +2194,12 @@ namespace Barotrauma lowPassMultiplier = MathHelper.Lerp(lowPassMultiplier, 1.0f, 0.1f); //ragdoll button - if (IsRagdolled) + if (IsRagdolled || !CanMove) { - if (AnimController is HumanoidAnimController) ((HumanoidAnimController)AnimController).Crouching = false; - /*if(GameMain.Server != null) - GameMain.Server.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.Status });*/ + if (AnimController is HumanoidAnimController) + { + ((HumanoidAnimController)AnimController).Crouching = false; + } AnimController.ResetPullJoints(); SelectedConstruction = null; return; @@ -2165,40 +2222,51 @@ namespace Barotrauma SelectedConstruction = null; } - if (!IsDead) LockHands = false; + if (!IsDead) { LockHands = false; } } partial void UpdateControlled(float deltaTime, Camera cam); partial void UpdateProjSpecific(float deltaTime, Camera cam); + partial void SetOrderProjSpecific(Order order, string orderOption); + private void UpdateOxygen(float deltaTime) { - PressureProtection -= deltaTime * 100.0f; - float hullAvailableOxygen = 0.0f; - if (!AnimController.HeadInWater && AnimController.CurrentHull != null) + if (NeedsAir) { - //don't decrease the amount of oxygen in the hull if the character has more oxygen available than the hull - //(i.e. if the character has some external source of oxygen) - if (OxygenAvailable * 0.98f < AnimController.CurrentHull.OxygenPercentage) + PressureProtection -= deltaTime * 100.0f; + } + if (NeedsWater) + { + float waterAvailable = 100; + if (!AnimController.InWater && CurrentHull != null) { - AnimController.CurrentHull.Oxygen -= Hull.OxygenConsumptionSpeed * deltaTime; + waterAvailable = CurrentHull.WaterPercentage; } - hullAvailableOxygen = AnimController.CurrentHull.OxygenPercentage; + OxygenAvailable += MathHelper.Clamp(waterAvailable - oxygenAvailable, -deltaTime * 50.0f, deltaTime * 50.0f); + } + else + { + float hullAvailableOxygen = 0.0f; + if (!AnimController.HeadInWater && AnimController.CurrentHull != null) + { + //don't decrease the amount of oxygen in the hull if the character has more oxygen available than the hull + //(i.e. if the character has some external source of oxygen) + if (OxygenAvailable * 0.98f < AnimController.CurrentHull.OxygenPercentage) + { + AnimController.CurrentHull.Oxygen -= Hull.OxygenConsumptionSpeed * deltaTime; + } + hullAvailableOxygen = AnimController.CurrentHull.OxygenPercentage; + + } + OxygenAvailable += MathHelper.Clamp(hullAvailableOxygen - oxygenAvailable, -deltaTime * 50.0f, deltaTime * 50.0f); } - OxygenAvailable += MathHelper.Clamp(hullAvailableOxygen - oxygenAvailable, -deltaTime * 50.0f, deltaTime * 50.0f); } + partial void UpdateOxygenProjSpecific(float prevOxygen); - private void UpdateUnconscious() - { - Stun = Math.Max(5.0f, Stun); - - AnimController.ResetPullJoints(); - SelectedConstruction = null; - } - /// /// How far the character is from the closest human player (including spectators) /// @@ -2231,23 +2299,44 @@ namespace Barotrauma } private float despawnTimer; - private const float DespawnDelay = 5.0f * 60.0f; //5 minutes private void UpdateDespawn(float deltaTime) { + if (!EnableDespawn) { return; } + //clients don't despawn characters unless the server says so if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; } if (!IsDead) { return; } + int subCorpseCount = 0; + + if (Submarine != null) + { + subCorpseCount = CharacterList.Count(c => c.IsDead && c.Submarine == Submarine); + if (subCorpseCount < GameMain.Config.CorpsesPerSubDespawnThreshold) { return; } + } + float distToClosestPlayer = GetDistanceToClosestPlayer(); if (distToClosestPlayer > NetConfig.DisableCharacterDist) { - //despawn in 1 second if very far from all human players - despawnTimer = Math.Max(despawnTimer, DespawnDelay - 1.0f); + //despawn in 1 minute if very far from all human players + despawnTimer = Math.Max(despawnTimer, GameMain.Config.CorpseDespawnDelay - 60.0f); } - despawnTimer += deltaTime; - if (despawnTimer < DespawnDelay) { return; } + float despawnPriority = 1.0f; + if (subCorpseCount > GameMain.Config.CorpsesPerSubDespawnThreshold) + { + //despawn faster if there are lots of corpses in the sub (twice as many as the threshold -> despawn twice as fast) + despawnPriority += (subCorpseCount - GameMain.Config.CorpsesPerSubDespawnThreshold) / (float)GameMain.Config.CorpsesPerSubDespawnThreshold; + } + if (AIController is EnemyAIController) + { + //enemies despawn faster + despawnPriority *= 2.0f; + } + + despawnTimer += deltaTime * despawnPriority; + if (despawnTimer < GameMain.Config.CorpseDespawnDelay) { return; } if (IsHuman) { @@ -2274,7 +2363,12 @@ namespace Barotrauma if (itemContainer == null) { return; } foreach (Item inventoryItem in Inventory.Items) { - itemContainer.Inventory.TryPutItem(inventoryItem, user: null); + if (inventoryItem == null) { continue; } + if (!itemContainer.Inventory.TryPutItem(inventoryItem, user: null)) + { + //if the item couldn't be put inside the despawn container, just drop it + inventoryItem.Drop(dropper: this); + } } } } @@ -2284,7 +2378,7 @@ namespace Barotrauma public void DespawnNow() { - despawnTimer = DespawnDelay; + despawnTimer = GameMain.Config.CorpseDespawnDelay; } public static void RemoveByPrefab(CharacterPrefab prefab) @@ -2350,18 +2444,29 @@ namespace Barotrauma //set the character order only if the character is close enough to hear the message if (orderGiver != null && !CanHearCharacter(orderGiver)) { return; } + // If there's another character operating the same device, make them dismiss themself + if (order != null && order.Category == OrderCategory.Operate) + { + CharacterList.FindAll(c => c != this && c.TeamID == TeamID && c.CurrentOrder is Order characterOrder && characterOrder.Category == OrderCategory.Operate && + characterOrder.Identifier.Equals(order.Identifier) && characterOrder.TargetEntity == order.TargetEntity)? + .ForEach(c => c.SetOrder(Order.GetPrefab("dismissed"), null, c, speak: true)); + } + if (AIController is HumanAIController humanAI) { humanAI.SetOrder(order, orderOption, orderGiver, speak); } -#if CLIENT - GameMain.GameSession?.CrewManager?.DisplayCharacterOrder(this, order, orderOption); -#endif - + SetOrderProjSpecific(order, orderOption); CurrentOrder = order; + CurrentOrderOption = orderOption; } + /// + /// Reset order data so it doesn't carry into further rounds, as the AI is "recreated" always in between rounds anyway. + /// + public void ResetCurrentOrder() => Info?.ResetCurrentOrder(); + private readonly List aiChatMessageQueue = new List(); //key = identifier, value = time the message was sent @@ -2511,7 +2616,7 @@ namespace Barotrauma if (attacker is Character attackingCharacter && attackingCharacter.AIController == null) { StringBuilder sb = new StringBuilder(); - sb.Append(LogName + " attacked by " + attackingCharacter.LogName + "."); + sb.Append(GameServer.CharacterLogName(this) + " attacked by " + GameServer.CharacterLogName(attackingCharacter) + "."); if (attackResult.Afflictions != null) { foreach (Affliction affliction in attackResult.Afflictions) @@ -2632,13 +2737,19 @@ namespace Barotrauma mainLimb.body.ApplyLinearImpulse(impulse, hitPos, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); } } + bool wasDead = IsDead; Vector2 simPos = hitLimb.SimPosition + ConvertUnits.ToSimUnits(dir); AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound); CharacterHealth.ApplyDamage(hitLimb, attackResult); + ApplyStatusEffects(ActionType.OnDamaged, 1.0f); if (attacker != this) { OnAttacked?.Invoke(attacker, attackResult); - OnAttackedProjSpecific(attacker, attackResult); + OnAttackedProjSpecific(attacker, attackResult, stun); + if (!wasDead) + { + TryAdjustAttackerSkill(attacker, -attackResult.Damage); + } }; if (attacker != null && attackResult.Damage > 0.0f) @@ -2649,7 +2760,31 @@ namespace Barotrauma return attackResult; } - partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult); + partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult, float stun); + + public void TryAdjustAttackerSkill(Character attacker, float healthChange) + { + if (attacker == null) { return; } + + bool isEnemy = AIController is EnemyAIController || TeamID != attacker.TeamID; + if (isEnemy) + { + if (healthChange < 0.0f) + { + float attackerSkillLevel = attacker.GetSkillLevel("weapons"); + attacker.Info?.IncreaseSkillLevel("weapons", + -healthChange * SkillSettings.Current.SkillIncreasePerHostileDamage / Math.Max(attackerSkillLevel, 1.0f), + attacker.WorldPosition + Vector2.UnitY * 100.0f); + } + } + else if (healthChange > 0.0f) + { + float attackerSkillLevel = attacker.GetSkillLevel("medical"); + attacker.Info?.IncreaseSkillLevel("medical", + healthChange * SkillSettings.Current.SkillIncreasePerFriendlyHealed / Math.Max(attackerSkillLevel, 1.0f), + attacker.WorldPosition + Vector2.UnitY * 100.0f); + } + } public void SetStun(float newStun, bool allowStunDecrease = false, bool isNetworkMessage = false) { @@ -2732,7 +2867,7 @@ namespace Barotrauma partial void ImplodeFX(); - public void Kill(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool isNetworkMessage = false) + public void Kill(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool isNetworkMessage = false, bool log = true) { if (IsDead || CharacterHealth.Unkillable) { return; } @@ -2775,9 +2910,9 @@ namespace Barotrauma SteamAchievementManager.OnCharacterKilled(this, CauseOfDeath); - KillProjSpecific(causeOfDeath, causeOfDeathAffliction); + KillProjSpecific(causeOfDeath, causeOfDeathAffliction, log); - if (info != null) info.CauseOfDeath = CauseOfDeath; + if (info != null) { info.CauseOfDeath = CauseOfDeath; } AnimController.movement = Vector2.Zero; AnimController.TargetMovement = Vector2.Zero; @@ -2800,7 +2935,7 @@ namespace Barotrauma GameMain.GameSession.KillCharacter(this); } } - partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction); + partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool log); public void Revive() { @@ -2930,13 +3065,13 @@ namespace Barotrauma public IEnumerable GetAttackContexts() { currentContexts.Clear(); - if (AnimController.CurrentAnimationParams.IsGroundedAnimation) + if (AnimController.InWater) { - currentContexts.Add(AttackContext.Ground); + currentContexts.Add(AttackContext.Water); } else { - currentContexts.Add(AttackContext.Water); + currentContexts.Add(AttackContext.Ground); } if (CurrentHull == null) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index 37d41d47e..f2e23373f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -293,6 +293,9 @@ namespace Barotrauma private NPCPersonalityTrait personalityTrait; + public Order CurrentOrder { get; set;} + public string CurrentOrderOption { get; set; } + //unique ID given to character infos in MP //used by clients to identify which infos are the same to prevent duplicate characters in round summary public ushort ID; @@ -524,9 +527,11 @@ namespace Barotrauma } foreach (XElement subElement in infoElement.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "job") continue; - Job = new Job(subElement); - break; + if (subElement.Name.ToString().Equals("job", StringComparison.OrdinalIgnoreCase)) + { + Job = new Job(subElement); + break; + } } LoadHeadAttachments(); } @@ -661,7 +666,7 @@ namespace Barotrauma { foreach (XElement limbElement in Ragdoll.MainElement.Elements()) { - if (limbElement.GetAttributeString("type", "").ToLowerInvariant() != "head") { continue; } + if (!limbElement.GetAttributeString("type", "").Equals("head", StringComparison.OrdinalIgnoreCase)) { continue; } XElement spriteElement = limbElement.Element("sprite"); if (spriteElement == null) { continue; } @@ -677,7 +682,7 @@ namespace Barotrauma //go through the files in the directory to find a matching sprite foreach (string file in Directory.GetFiles(Path.GetDirectoryName(spritePath))) { - if (!file.EndsWith(".png", StringComparison.InvariantCultureIgnoreCase)) + if (!file.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) { continue; } @@ -828,6 +833,11 @@ namespace Barotrauma { if (Job == null || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) || Character == null) { return; } + if (Job.Prefab.Identifier == "assistant") + { + increase *= SkillSettings.Current.AssistantSkillIncreaseMultiplier; + } + float prevLevel = Job.GetSkillLevel(skillIdentifier); Job.IncreaseSkillLevel(skillIdentifier, increase); @@ -955,7 +965,7 @@ namespace Barotrauma foreach (XElement childInvElement in itemElement.Elements()) { if (itemContainerIndex >= itemContainers.Count) break; - if (childInvElement.Name.ToString().ToLowerInvariant() != "inventory") continue; + if (!childInvElement.Name.ToString().Equals("inventory", StringComparison.OrdinalIgnoreCase)) { continue; } SpawnInventoryItemsRecursive(itemContainers[itemContainerIndex].Inventory, childInvElement); itemContainerIndex++; } @@ -987,6 +997,15 @@ namespace Barotrauma faceAttachments = null; } + /// + /// Reset order data so it doesn't carry into further rounds, as the AI is "recreated" always in between rounds anyway. + /// + public void ResetCurrentOrder() + { + CurrentOrder = null; + CurrentOrderOption = ""; + } + public void Remove() { Character = null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs index d8f268ea5..9652a54ad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs @@ -19,7 +19,7 @@ namespace Barotrauma public virtual float Strength { get { return _strength; } - set { _strength = value; } + set { _strength = MathHelper.Clamp(value, 0.0f, Prefab.MaxStrength); } } [Serialize("", true), Editable] @@ -184,6 +184,8 @@ namespace Barotrauma { _strength += currentEffect.StrengthChange * deltaTime * (1f - characterHealth.GetResistance(Prefab.Identifier)); } + // Don't use the property, because its virtual and some afflictions like husk overload it for external use. + _strength = MathHelper.Clamp(_strength, 0.0f, Prefab.MaxStrength); foreach (StatusEffect statusEffect in currentEffect.StatusEffects) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs index cc0509492..ed9c2360d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionHusk.cs @@ -101,9 +101,9 @@ namespace Barotrauma { float random = Rand.Value(Rand.RandSync.Server); huskInfection.Clear(); - huskInfection.Add(AfflictionPrefab.InternalDamage.Instantiate(random * deltaTime / character.AnimController.Limbs.Length)); + huskInfection.Add(AfflictionPrefab.InternalDamage.Instantiate(random * 10 * deltaTime / character.AnimController.Limbs.Length)); character.LastDamageSource = null; - float force = applyForce ? random * 0.1f * limb.Mass : 0; + float force = applyForce ? random * 0.5f * limb.Mass : 0; character.DamageLimb(limb.WorldPosition, limb, huskInfection, 0, false, force); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index 5785866d5..7c90aa161 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -265,7 +265,8 @@ namespace Barotrauma public readonly Sprite Icon; public readonly Color[] IconColors; - private List effects = new List(); + private readonly List effects = new List(); + public IEnumerable Effects => effects; private readonly string typeName; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index 3d0ac4788..55bd1db75 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -114,6 +114,10 @@ namespace Barotrauma private List limbHealths = new List(); //non-limb-specific afflictions private List afflictions = new List(); + /// + /// Note: returns only the non-limb-secific afflictions. Use GetAllAfflictions or some other method for getting also the limb-specific afflictions. + /// + public IEnumerable Afflictions => afflictions; private HashSet irremovableAfflictions = new HashSet(); private Affliction bloodlossAffliction; @@ -160,12 +164,12 @@ namespace Barotrauma { get { - if (!Character.NeedsAir || Unkillable) return 100.0f; + if (!Character.NeedsOxygen || Unkillable) { return 100.0f; } return -oxygenLowAffliction.Strength + 100; } set { - if (!Character.NeedsAir || Unkillable) return; + if (!Character.NeedsOxygen || Unkillable) { return; } oxygenLowAffliction.Strength = MathHelper.Clamp(-value + 100, 0.0f, 200.0f); } } @@ -216,7 +220,7 @@ namespace Barotrauma limbHealths.Clear(); foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "limb") continue; + if (!subElement.Name.ToString().Equals("limb", StringComparison.OrdinalIgnoreCase)) { continue; } limbHealths.Add(new LimbHealth(subElement, this)); } if (limbHealths.Count == 0) @@ -269,30 +273,6 @@ namespace Barotrauma } } - public Affliction GetAffliction(string identifier, bool allowLimbAfflictions = true) - { - foreach (Affliction affliction in afflictions) - { - if (affliction.Prefab.Identifier == identifier) return affliction; - } - if (!allowLimbAfflictions) return null; - - foreach (LimbHealth limbHealth in limbHealths) - { - foreach (Affliction affliction in limbHealth.Afflictions) - { - if (affliction.Prefab.Identifier == identifier) return affliction; - } - } - - return null; - } - - public T GetAffliction(string identifier, bool allowLimbAfflictions = true) where T : Affliction - { - return GetAffliction(identifier, allowLimbAfflictions) as T; - } - public IEnumerable GetAfflictionsByType(string afflictionType, Limb limb) { if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count) @@ -304,6 +284,37 @@ namespace Barotrauma return limbHealths[limb.HealthIndex].Afflictions.Where(a => a.Prefab.AfflictionType == afflictionType); } + public Affliction GetAffliction(string identifier, bool allowLimbAfflictions = true) + => GetAffliction(a => a.Prefab.Identifier == identifier, allowLimbAfflictions); + + public Affliction GetAfflictionOfType(string afflictionType, bool allowLimbAfflictions = true) + => GetAffliction(a => a.Prefab.AfflictionType == afflictionType, allowLimbAfflictions); + + private Affliction GetAffliction(Func predicate, bool allowLimbAfflictions = true) + { + foreach (Affliction affliction in afflictions) + { + if (predicate(affliction)) { return affliction; } + } + if (!allowLimbAfflictions) + { + return null; + } + foreach (LimbHealth limbHealth in limbHealths) + { + foreach (Affliction affliction in limbHealth.Afflictions) + { + if (predicate(affliction)) { return affliction; } + } + } + return null; + } + + public T GetAffliction(string identifier, bool allowLimbAfflictions = true) where T : Affliction + { + return GetAffliction(identifier, allowLimbAfflictions) as T; + } + public Affliction GetAffliction(string identifier, Limb limb) { if (limb.HealthIndex < 0 || limb.HealthIndex >= limbHealths.Count) @@ -408,12 +419,11 @@ namespace Barotrauma return resistance; } + private readonly List matchingAfflictions = new List(); public void ReduceAffliction(Limb targetLimb, string affliction, float amount) { - affliction = affliction.ToLowerInvariant(); - - List matchingAfflictions = new List(afflictions); - + matchingAfflictions.Clear(); + matchingAfflictions.AddRange(afflictions); if (targetLimb != null) { matchingAfflictions.AddRange(limbHealths[targetLimb.HealthIndex].Afflictions); @@ -426,8 +436,8 @@ namespace Barotrauma } } matchingAfflictions.RemoveAll(a => - a.Prefab.Identifier.ToLowerInvariant() != affliction && - a.Prefab.AfflictionType.ToLowerInvariant() != affliction); + !a.Prefab.Identifier.Equals(affliction, StringComparison.OrdinalIgnoreCase) && + !a.Prefab.AfflictionType.Equals(affliction, StringComparison.OrdinalIgnoreCase)); if (matchingAfflictions.Count == 0) return; @@ -526,7 +536,7 @@ namespace Barotrauma private void AddLimbAffliction(LimbHealth limbHealth, Affliction newAffliction) { if (!DoesBleed && newAffliction is AfflictionBleeding) return; - if (!Character.NeedsAir && newAffliction.Prefab == AfflictionPrefab.OxygenLow) return; + if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) return; foreach (Affliction affliction in limbHealth.Afflictions) { @@ -559,7 +569,7 @@ namespace Barotrauma private void AddAffliction(Affliction newAffliction) { if (!DoesBleed && newAffliction is AfflictionBleeding) return; - if (!Character.NeedsAir && newAffliction.Prefab == AfflictionPrefab.OxygenLow) return; + if (!Character.NeedsOxygen && newAffliction.Prefab == AfflictionPrefab.OxygenLow) return; if (newAffliction.Prefab.AfflictionType == "huskinfection") { var huskPrefab = newAffliction.Prefab as AfflictionPrefabHusk; @@ -653,7 +663,7 @@ namespace Barotrauma private void UpdateOxygen(float deltaTime) { - if (!Character.NeedsAir) return; + if (!Character.NeedsOxygen) { return; } float prevOxygen = OxygenAmount; if (IsUnconscious) @@ -691,13 +701,15 @@ namespace Barotrauma foreach (Affliction affliction in limbHealth.Afflictions) { float vitalityDecrease = affliction.GetVitalityDecrease(this); - if (limbHealth.VitalityMultipliers.ContainsKey(affliction.Prefab.Identifier.ToLowerInvariant())) + string identifier = affliction.Prefab.Identifier.ToLowerInvariant(); + string type = affliction.Prefab.AfflictionType.ToLowerInvariant(); + if (limbHealth.VitalityMultipliers.ContainsKey(identifier)) { - vitalityDecrease *= limbHealth.VitalityMultipliers[affliction.Prefab.Identifier.ToLowerInvariant()]; + vitalityDecrease *= limbHealth.VitalityMultipliers[identifier]; } - if (limbHealth.VitalityTypeMultipliers.ContainsKey(affliction.Prefab.AfflictionType.ToLowerInvariant())) + if (limbHealth.VitalityTypeMultipliers.ContainsKey(type)) { - vitalityDecrease *= limbHealth.VitalityTypeMultipliers[affliction.Prefab.AfflictionType.ToLowerInvariant()]; + vitalityDecrease *= limbHealth.VitalityTypeMultipliers[type]; } vitalityDecrease *= damageResistanceMultiplier; Vitality -= vitalityDecrease; @@ -750,6 +762,7 @@ namespace Barotrauma return new Pair(causeOfDeath, strongestAffliction); } + // TODO: this method is called a lot (every half second) -> optimize, don't create new class instances and lists every time! private List GetAllAfflictions(bool mergeSameAfflictions) { List allAfflictions = new List(afflictions); @@ -832,10 +845,18 @@ namespace Barotrauma } } + private readonly List activeAfflictions = new List(); + private readonly List> limbAfflictions = new List>(); public void ServerWrite(IWriteMessage msg) { - List activeAfflictions = afflictions.FindAll(a => a.Strength > 0.0f && a.Strength >= a.Prefab.ActivationThreshold); - + activeAfflictions.Clear(); + foreach (var affliction in afflictions) + { + if (affliction.Strength > 0.0f && affliction.Strength >= affliction.Prefab.ActivationThreshold) + { + activeAfflictions.Add(affliction); + } + } msg.Write((byte)activeAfflictions.Count); foreach (Affliction affliction in activeAfflictions) { @@ -845,7 +866,7 @@ namespace Barotrauma 0.0f, affliction.Prefab.MaxStrength, 8); } - List> limbAfflictions = new List>(); + limbAfflictions.Clear(); foreach (LimbHealth limbHealth in limbHealths) { foreach (Affliction limbAffliction in limbHealth.Afflictions) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs index 7439a6f30..bffa27eca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/DamageModifier.cs @@ -19,6 +19,13 @@ namespace Barotrauma private set; } + [Serialize(1.0f, false), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = 1)] + public float ProbabilityMultiplier + { + get; + private set; + } + [Serialize("0.0,360", false), Editable] public Vector2 ArmorSector { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs index 85bb5925c..a257c66d3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/Job.cs @@ -62,7 +62,7 @@ namespace Barotrauma skills = new Dictionary(); foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "skill") { continue; } + if (!subElement.Name.ToString().Equals("skill", System.StringComparison.OrdinalIgnoreCase)) { continue; } string skillIdentifier = subElement.GetAttributeString("identifier", ""); if (string.IsNullOrEmpty(skillIdentifier)) { continue; } skills.Add( @@ -80,8 +80,8 @@ namespace Barotrauma public float GetSkillLevel(string skillIdentifier) { + if (string.IsNullOrWhiteSpace(skillIdentifier)) { return 0.0f; } skills.TryGetValue(skillIdentifier, out Skill skill); - return (skill == null) ? 0.0f : skill.Level; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs index fa530a09e..3b738e1b9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Jobs/JobPrefab.cs @@ -146,6 +146,7 @@ namespace Barotrauma private set; } + // TODO: not used [Serialize(10.0f, false)] public float Commonness { @@ -262,7 +263,7 @@ namespace Barotrauma } foreach (XElement element in mainElement.Elements()) { - if (element.Name.ToString().ToLowerInvariant() == "nojob") { continue; } + if (element.Name.ToString().Equals("nojob", StringComparison.OrdinalIgnoreCase)) { continue; } if (element.IsOverride()) { var job = new JobPrefab(element.FirstElement(), file.Path) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index b990d25d7..cfd1a8b81 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -366,6 +366,8 @@ namespace Barotrauma public string Name => Params.Name; + public bool IsDead => character.IsDead; + public Dictionary SerializableProperties { get; @@ -471,43 +473,59 @@ namespace Barotrauma return AddDamage(simPosition, afflictions, playSound); } + private readonly List appliedDamageModifiers = new List(); + private readonly List afflictionsCopy = new List(); public AttackResult AddDamage(Vector2 simPosition, IEnumerable afflictions, bool playSound) { - List appliedDamageModifiers = new List(); - //create a copy of the original affliction list to prevent modifying the afflictions of an Attack/StatusEffect etc - var afflictionsCopy = afflictions.Where(a => Rand.Range(0.0f, 1.0f) <= a.Probability).ToList(); - for (int i = 0; i < afflictionsCopy.Count; i++) + appliedDamageModifiers.Clear(); + afflictionsCopy.Clear(); + foreach (var affliction in afflictions) { + var newAffliction = affliction; + float random = Rand.Value(Rand.RandSync.Unsynced); + if (random > affliction.Probability) { continue; } + bool applyAffliction = true; foreach (DamageModifier damageModifier in DamageModifiers) { - if (!damageModifier.MatchesAffliction(afflictionsCopy[i])) continue; + if (!damageModifier.MatchesAffliction(affliction)) { continue; } + if (random > affliction.Probability * damageModifier.ProbabilityMultiplier) + { + applyAffliction = false; + continue; + } if (SectorHit(damageModifier.ArmorSectorInRadians, simPosition)) { - afflictionsCopy[i] = afflictionsCopy[i].CreateMultiplied(damageModifier.DamageMultiplier); + newAffliction = affliction.CreateMultiplied(damageModifier.DamageMultiplier); appliedDamageModifiers.Add(damageModifier); } } - foreach (WearableSprite wearable in wearingItems) { foreach (DamageModifier damageModifier in wearable.WearableComponent.DamageModifiers) { - if (!damageModifier.MatchesAffliction(afflictionsCopy[i])) continue; + if (!damageModifier.MatchesAffliction(affliction)) { continue; } + if (random > affliction.Probability * damageModifier.ProbabilityMultiplier) + { + applyAffliction = false; + continue; + } if (SectorHit(damageModifier.ArmorSectorInRadians, simPosition)) { - afflictionsCopy[i] = afflictionsCopy[i].CreateMultiplied(damageModifier.DamageMultiplier); + newAffliction = affliction.CreateMultiplied(damageModifier.DamageMultiplier); appliedDamageModifiers.Add(damageModifier); } } } + if (applyAffliction) + { + afflictionsCopy.Add(newAffliction); + } } - - AddDamageProjSpecific(simPosition, afflictionsCopy, playSound, appliedDamageModifiers); - + AddDamageProjSpecific(afflictionsCopy, playSound, appliedDamageModifiers); return new AttackResult(afflictionsCopy, this, appliedDamageModifiers); } - partial void AddDamageProjSpecific(Vector2 simPosition, List afflictions, bool playSound, List appliedDamageModifiers); + partial void AddDamageProjSpecific(IEnumerable afflictions, bool playSound, IEnumerable appliedDamageModifiers); public bool SectorHit(Vector2 armorSector, Vector2 simPosition) { @@ -646,33 +664,19 @@ namespace Barotrauma if (wasHit) { - bool playSound = false; -#if CLIENT - playSound = LastAttackSoundTime < Timing.TotalTime - SoundInterval; - if (playSound) + if (character == Character.Controlled || GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) { - LastAttackSoundTime = SoundInterval; + ExecuteAttack(damageTarget, targetLimb, out attackResult); } +#if SERVER + GameMain.NetworkMember.CreateEntityEvent(character, new object[] + { + NetEntityEvent.Type.ExecuteAttack, + this, + (damageTarget as Entity)?.ID ?? Entity.NullEntityID, + damageTarget is Character && targetLimb != null ? Array.IndexOf(((Character)damageTarget).AnimController.Limbs, targetLimb) : 0 + }); #endif - if (damageTarget is Character targetCharacter && targetLimb != null) - { - attackResult = attack.DoDamageToLimb(character, targetLimb, WorldPosition, 1.0f, playSound); - } - else - { - attackResult = attack.DoDamage(character, damageTarget, WorldPosition, 1.0f, playSound); - } - if (structureBody != null && attack.StickChance > Rand.Range(0.0f, 1.0f, Rand.RandSync.Server)) - { - // TODO: use the hit pos? - var localFront = body.GetLocalFront(Params.GetSpriteOrientation()); - var from = body.FarseerBody.GetWorldPoint(localFront); - var to = from; - var drawPos = body.DrawPosition; - StickTo(structureBody, from, to); - } - attack.ResetAttackTimer(); - attack.SetCoolDown(); } Vector2 diff = attackSimPos - SimPosition; @@ -705,11 +709,49 @@ namespace Barotrauma if (!attack.IsRunning) { // Set the main collider where the body lands after the attack - character.AnimController.Collider.SetTransform(character.AnimController.MainLimb.body.SimPosition, rotation: 0); + character.AnimController.Collider.SetTransform(character.AnimController.MainLimb.body.SimPosition, rotation: character.AnimController.Collider.Rotation); } return wasHit; } + public void ExecuteAttack(IDamageable damageTarget, Limb targetLimb, out AttackResult attackResult) + { + bool playSound = false; +#if CLIENT + playSound = LastAttackSoundTime < Timing.TotalTime - SoundInterval; + if (playSound) + { + LastAttackSoundTime = SoundInterval; + } +#endif + if (damageTarget is Character targetCharacter && targetLimb != null) + { + attackResult = attack.DoDamageToLimb(character, targetLimb, WorldPosition, 1.0f, playSound); + } + else + { + if (damageTarget is Item targetItem && !targetItem.Prefab.DamagedByMonsters) + { + attackResult = new AttackResult(); + } + else + { + attackResult = attack.DoDamage(character, damageTarget, WorldPosition, 1.0f, playSound); + } + } + /*if (structureBody != null && attack.StickChance > Rand.Range(0.0f, 1.0f, Rand.RandSync.Server)) + { + // TODO: use the hit pos? + var localFront = body.GetLocalFront(Params.GetSpriteOrientation()); + var from = body.FarseerBody.GetWorldPoint(localFront); + var to = from; + var drawPos = body.DrawPosition; + StickTo(structureBody, from, to); + }*/ + attack.ResetAttackTimer(); + attack.SetCoolDown(); + } + private WeldJoint attachJoint; private WeldJoint colliderJoint; public bool IsStuck => attachJoint != null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs index 91c4e209d..9f97ec0be 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Animation/AnimationParams.cs @@ -125,7 +125,7 @@ namespace Barotrauma public static string GetFolder(XDocument doc, string filePath) { var folder = doc.Root?.Element("animations")?.GetAttributeString("folder", string.Empty); - if (string.IsNullOrEmpty(folder) || folder.ToLowerInvariant() == "default") + if (string.IsNullOrEmpty(folder) || folder.Equals("default", StringComparison.OrdinalIgnoreCase)) { folder = Path.Combine(Path.GetDirectoryName(filePath), "Animations"); } @@ -198,7 +198,7 @@ namespace Barotrauma } else { - selectedFile = filteredFiles.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).ToLowerInvariant() == fileName.ToLowerInvariant()); + selectedFile = filteredFiles.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase)); if (selectedFile == null) { DebugConsole.ThrowError($"[AnimationParams] Could not find an animation file that matches the name {fileName} and the animation type {animType}. Using the default animations."); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 04f17291e..9127cd833 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -40,6 +40,9 @@ namespace Barotrauma [Serialize(false, true), Editable] public bool NeedsAir { get; set; } + [Serialize(false, true, description: "Can the creature live without water or does it die on dry land?"), Editable] + public bool NeedsWater { get; set; } + [Serialize(false, true), Editable] public bool CanSpeak { get; set; } @@ -52,12 +55,21 @@ namespace Barotrauma [Serialize("blood", true), Editable] public string BloodDecal { get; private set; } + [Serialize("blooddrop", true), Editable] + public string BleedParticleAir { get; private set; } + + [Serialize("waterblood", true), Editable] + public string BleedParticleWater { get; private set; } + [Serialize(10f, true, description: "How effectively/easily the character eats other characters. Affects the forces, the amount of particles, and the time required before the target is eaten away"), Editable(MinValueFloat = 1, MaxValueFloat = 1000, ValueStep = 1)] public float EatingSpeed { get; set; } [Serialize(1f, true, "Decreases the intensive path finding call frequency. Set to a lower value for insignificant creatures to improve performance."), Editable(minValue: 0f, maxValue: 1f)] public float PathFinderPriority { get; set; } + [Serialize(false, true), Editable] + public bool HideInSonar { get; set; } + public readonly string File; public readonly List SubParams = new List(); @@ -462,6 +474,12 @@ namespace Barotrauma [Serialize(true, true, description: "Enforce aggressive behavior if the creature is spawned as a target of a monster mission."), Editable()] public bool EnforceAggressiveBehaviorForMissions { get; private set; } + [Serialize(false, true, description: "Should the character target or ignore walls when it's inside the submarine. Doesn't have any effect if no target priority for walls is defined."), Editable()] + public bool TargetInnerWalls { get; private set; } + + [Serialize(false, true, description: "If enabled, the character chooses randomly from the available attacks. The priority is used as a weight for weighted random."), Editable()] + public bool RandomAttack { get; private set; } + // TODO: latchonto, swarming public IEnumerable Targets => targets; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs index 70d687487..ad3f4ba54 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/Ragdoll/RagdollParams.cs @@ -94,8 +94,7 @@ namespace Barotrauma public static string GetFolder(string speciesName, ContentPackage contentPackage = null) { - CharacterPrefab prefab = CharacterPrefab.Find(p => p.Identifier.ToLowerInvariant()==speciesName.ToLowerInvariant() && - (contentPackage==null || p.ContentPackage == contentPackage)); + CharacterPrefab prefab = CharacterPrefab.Find(p => p.Identifier.Equals(speciesName, StringComparison.OrdinalIgnoreCase) && (contentPackage == null || p.ContentPackage == contentPackage)); if (prefab?.XDocument == null) { DebugConsole.ThrowError($"Failed to find config file for '{speciesName}' (content package {contentPackage?.Name ?? "null"})"); @@ -107,7 +106,7 @@ namespace Barotrauma public static string GetFolder(XDocument doc, string filePath) { var folder = doc.Root?.Element("ragdolls")?.GetAttributeString("folder", string.Empty); - if (string.IsNullOrEmpty(folder) || folder.ToLowerInvariant() == "default") + if (string.IsNullOrEmpty(folder) || folder.Equals("default", StringComparison.OrdinalIgnoreCase)) { folder = Path.Combine(Path.GetDirectoryName(filePath), "Ragdolls") + Path.DirectorySeparatorChar; } @@ -150,7 +149,7 @@ namespace Barotrauma } else { - selectedFile = files.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).ToLowerInvariant() == fileName.ToLowerInvariant()); + selectedFile = files.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase)); if (selectedFile == null) { DebugConsole.ThrowError($"[RagdollParams] Could not find a ragdoll file that matches the name {fileName}. Using the default ragdoll."); @@ -814,6 +813,15 @@ namespace Barotrauma [Serialize("", true), Editable()] public string Texture { get; set; } + [Serialize("1.0,1.0,1.0,1.0", true), Editable()] + public Color Color { get; set; } + + [Serialize("1.0,1.0,1.0,1.0", true, description: "Target color when the character is dead."), Editable()] + public Color DeadColor { get; set; } + + [Serialize(0f, true, "How long it takes to fade into the dead color? 0 = Not applied."), Editable(DecimalCount = 1, MinValueFloat = 0, MaxValueFloat = 10)] + public float DeadColorTime { get; set; } + public override string Name => "Sprite"; public SpriteParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs index f0814daed..abe6aab58 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/SkillSettings.cs @@ -65,6 +65,37 @@ namespace Barotrauma set { skillIncreasePerFabricatorRequiredSkill = value; } } + private float skillIncreasePerHostileDamage; + [Serialize(0.01f, true)] + public float SkillIncreasePerHostileDamage + { + get { return skillIncreasePerHostileDamage * GetCurrentSkillGainMultiplier(); } + set { skillIncreasePerHostileDamage = value; } + } + + private float skillIncreasePerSecondWhenOperatingTurret; + [Serialize(0.001f, true)] + public float SkillIncreasePerSecondWhenOperatingTurret + { + get { return skillIncreasePerSecondWhenOperatingTurret * GetCurrentSkillGainMultiplier(); } + set { skillIncreasePerSecondWhenOperatingTurret = value; } + } + + private float skillIncreasePerFriendlyHealed; + [Serialize(0.001f, true)] + public float SkillIncreasePerFriendlyHealed + { + get { return skillIncreasePerFriendlyHealed * GetCurrentSkillGainMultiplier(); } + set { skillIncreasePerFriendlyHealed = value; } + } + + [Serialize(1.1f, true)] + public float AssistantSkillIncreaseMultiplier + { + get; + set; + } + private SkillSettings(XElement element) { SerializableProperties = SerializableProperty.DeserializeProperties(this, element); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs index 84f727001..36768f311 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentPackage.cs @@ -39,7 +39,10 @@ namespace Barotrauma TraitorMissions, EventManagerSettings, Orders, - SkillSettings + SkillSettings, + Wreck, + Corpses, + WreckAIConfig } public class ContentPackage @@ -63,8 +66,11 @@ namespace Barotrauma ContentType.LevelObjectPrefabs, ContentType.RuinConfig, ContentType.Outpost, + ContentType.Wreck, + ContentType.WreckAIConfig, ContentType.Afflictions, - ContentType.Orders + ContentType.Orders, + ContentType.Corpses }; //at least one file of each these types is required in core content packages @@ -75,6 +81,8 @@ namespace Barotrauma ContentType.Character, ContentType.Structure, ContentType.Outpost, + ContentType.Wreck, + ContentType.WreckAIConfig, ContentType.Text, ContentType.Executable, ContentType.ServerExecutable, @@ -87,7 +95,8 @@ namespace Barotrauma ContentType.Afflictions, ContentType.UIStyle, ContentType.EventManagerSettings, - ContentType.Orders + ContentType.Orders, + ContentType.Corpses }; public static IEnumerable CorePackageRequiredFiles @@ -284,6 +293,7 @@ namespace Barotrauma case ContentType.None: case ContentType.Outpost: case ContentType.Submarine: + case ContentType.Wreck: break; default: try @@ -364,7 +374,10 @@ namespace Barotrauma { if (Files.Find(file => file.Path == path && file.Type == type) != null) return null; - ContentFile cf = new ContentFile(path, type); + ContentFile cf = new ContentFile(path, type) + { + ContentPackage = this + }; Files.Add(cf); return cf; @@ -508,14 +521,22 @@ namespace Barotrauma return IsModFilePathAllowed(path); } /// - /// Are mods allowed to install a file into the specified path. If a content package XML includes files - /// with a prohibited path, they are treated as references to external files. For example, a mod could include - /// some vanilla files in the XML, in which case the game will simply use the vanilla files present in the game folder. + /// Returns whether mods are allowed to install a file into the specified path. + /// Currently mods are only allowed to install files into the Mods folder. + /// The only exception to this rule is the Vanilla content package. /// /// /// public static bool IsModFilePathAllowed(string path) { + if (GameMain.VanillaContent.Files.Any(f => string.Equals(System.IO.Path.GetFullPath(f.Path).CleanUpPath(), + System.IO.Path.GetFullPath(path).CleanUpPath(), + StringComparison.InvariantCultureIgnoreCase))) + { + //file is in vanilla package, this is allowed + return true; + } + while (true) { string temp = System.IO.Path.GetDirectoryName(path); @@ -573,7 +594,13 @@ namespace Barotrauma { if (System.IO.Path.GetFileName(modDirectory.TrimEnd(System.IO.Path.DirectorySeparatorChar)) == "ExampleMod") { continue; } string modFilePath = System.IO.Path.Combine(modDirectory, Steam.SteamManager.MetadataFileName); - if (File.Exists(modFilePath)) + string copyingFilePath = System.IO.Path.Combine(modDirectory, Steam.SteamManager.CopyIndicatorFileName); + if (File.Exists(copyingFilePath)) + { + //this mod didn't clean up its copying file; assume it's corrupted and delete it + Directory.Delete(modDirectory, true); + } + else if (File.Exists(modFilePath)) { List.Add(new ContentPackage(modFilePath)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index af7938c46..b05f4bc9c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -475,10 +475,7 @@ namespace Barotrauma commands.Add(new Command("teleportcharacter|teleport", "teleport [character name]: Teleport the specified character to the position of the cursor. If the name parameter is omitted, the controlled character will be teleported.", null, () => { - return new string[][] - { - Character.CharacterList.Select(c => c.Name).Distinct().ToArray() - }; + return new string[][] { ListCharacterNames() }; }, isCheat: true)); commands.Add(new Command("godmode", "godmode: Toggle submarine godmode. Makes the main submarine invulnerable to damage.", (string[] args) => @@ -531,18 +528,17 @@ namespace Barotrauma commands.Add(new Command("findentityids", "findentityids [entityname]", (string[] args) => { - if (args.Length == 0) return; - args[0] = args[0].ToLowerInvariant(); + if (args.Length == 0) { return; } foreach (MapEntity mapEntity in MapEntity.mapEntityList) { - if (mapEntity.Name.ToLowerInvariant() == args[0]) + if (mapEntity.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase)) { ThrowError(mapEntity.ID + ": " + mapEntity.Name.ToString()); } } foreach (Character character in Character.CharacterList) { - if (character.Name.ToLowerInvariant() == args[0] || character.SpeciesName.ToLowerInvariant() == args[0]) + if (character.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase) || character.SpeciesName.Equals(args[0], StringComparison.OrdinalIgnoreCase)) { ThrowError(character.ID + ": " + character.Name.ToString()); } @@ -554,8 +550,8 @@ namespace Barotrauma if (args.Length < 2) return; AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a => - a.Name.ToLowerInvariant() == args[0].ToLowerInvariant() || - a.Identifier.ToLowerInvariant() == args[0].ToLowerInvariant()); + a.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase) || + a.Identifier.Equals(args[0], StringComparison.OrdinalIgnoreCase)); if (afflictionPrefab == null) { ThrowError("Affliction \"" + args[0] + "\" not found."); @@ -682,7 +678,7 @@ namespace Barotrauma NewMessage(Hull.EditFire ? "Fire spawning on" : "Fire spawning off", Color.White); }, isCheat: true)); - commands.Add(new Command("explosion", "explosion [range] [force] [damage] [structuredamage] [emp strength]: Creates an explosion at the position of the cursor.", null, isCheat: true)); + commands.Add(new Command("explosion", "explosion [range] [force] [damage] [structuredamage] [item damage] [emp strength]: Creates an explosion at the position of the cursor.", null, isCheat: true)); commands.Add(new Command("showseed|showlevelseed", "showseed: Show the seed of the current level.", (string[] args) => { @@ -695,18 +691,20 @@ namespace Barotrauma NewMessage("Level seed: " + Level.Loaded.Seed); } },null)); - -#if DEBUG - commands.Add(new Command("crash", "crash: Crashes the game.", (string[] args) => - { - throw new Exception("crash command issued"); - })); - + commands.Add(new Command("teleportsub", "teleportsub [start/end]: Teleport the submarine to the start or end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.", (string[] args) => { if (Submarine.MainSub == null || Level.Loaded == null) return; - if (args.Length > 0 && args[0].ToLowerInvariant() == "start") + if (args.Length == 0 || args[0].Equals("cursor", StringComparison.OrdinalIgnoreCase)) + { +#if SERVER + ThrowError("Cannot teleport the sub to the position of the cursor. Use \"start\" or \"end\", or execute the command as a client."); +#else + Submarine.MainSub.SetPosition(Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition)); +#endif + } + else if (args[0].Equals("start", StringComparison.OrdinalIgnoreCase)) { Submarine.MainSub.SetPosition(Level.Loaded.StartPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height); } @@ -714,8 +712,21 @@ namespace Barotrauma { Submarine.MainSub.SetPosition(Level.Loaded.EndPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height); } + }, + () => + { + return new string[][] + { + new string[] { "start", "end", "cursor" } + }; }, isCheat: true)); +#if DEBUG + commands.Add(new Command("crash", "crash: Crashes the game.", (string[] args) => + { + throw new Exception("crash command issued"); + })); + commands.Add(new Command("removecharacter", "removecharacter [character name]: Immediately deletes the specified character.", (string[] args) => { if (args.Length == 0) { return; } @@ -751,18 +762,18 @@ namespace Barotrauma IEnumerable TestLevels() { - Submarine selectedSub = null; + SubmarineInfo selectedSub = null; string subName = GameMain.Config.QuickStartSubmarineName; if (!string.IsNullOrEmpty(subName)) { - selectedSub = Submarine.SavedSubmarines.FirstOrDefault(s => s.Name.ToLower() == subName.ToLower()); + selectedSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name.ToLower() == subName.ToLower()); } int count = 0; while (true) { var gamesession = new GameSession( - Submarine.SavedSubmarines.GetRandom(s => !s.HasTag(SubmarineTag.HideInMenus)), + SubmarineInfo.SavedSubmarines.GetRandom(s => !s.HasTag(SubmarineTag.HideInMenus)), "Data/Saves/test.xml", GameModePreset.List.Find(gm => gm.Identifier == "devsandbox"), missionPrefab: null); @@ -776,7 +787,7 @@ namespace Barotrauma { if (ruin.Area.Intersects(subWorldRect)) { - ThrowError("Ruins intersect with the sub. Seed: " + seed + ", Submarine: " + Submarine.MainSub.Name); + ThrowError("Ruins intersect with the sub. Seed: " + seed + ", Submarine: " + Submarine.MainSub.Info.Name); yield return CoroutineStatus.Success; } } @@ -797,7 +808,7 @@ namespace Barotrauma (int)(maxExtents.X - minExtents.X), (int)(maxExtents.Y - minExtents.Y)); if (cellRect.Intersects(subWorldRect)) { - ThrowError("Level cells intersect with the sub. Seed: " + seed + ", Submarine: " + Submarine.MainSub.Name); + ThrowError("Level cells intersect with the sub. Seed: " + seed + ", Submarine: " + Submarine.MainSub.Info.Name); yield return CoroutineStatus.Success; } } @@ -816,7 +827,7 @@ namespace Barotrauma } #endif - commands.Add(new Command("fixitems", "fixitems: Repairs all items and restores them to full condition.", (string[] args) => + commands.Add(new Command("fixitems", "fixitems: Repairs all items and restores them to full condition.", (string[] args) => { foreach (Item it in Item.ItemList) { @@ -1097,10 +1108,7 @@ namespace Barotrauma //TODO: alphabetical order? commands.Add(new Command("control", "control [character name]: Start controlling the specified character (client-only).", null, () => { - return new string[][] - { - Character.CharacterList.Select(c => c.Name).Distinct().ToArray() - }; + return new string[][] { ListCharacterNames() }; })); commands.Add(new Command("los", "Toggle the line of sight effect on/off (client-only).", null, isCheat: true)); commands.Add(new Command("lighting|lights", "Toggle lighting on/off (client-only).", null, isCheat: true)); @@ -1220,15 +1228,17 @@ namespace Barotrauma return; } - if (!splitCommand[0].ToLowerInvariant().Equals("admin")) + string firstCommand = splitCommand[0].ToLowerInvariant(); + + if (!firstCommand.Equals("admin", StringComparison.OrdinalIgnoreCase)) { NewMessage(command, Color.White, true); } - + #if CLIENT if (GameMain.Client != null) { - Command matchingCommand = commands.Find(c => c.names.Contains(splitCommand[0].ToLowerInvariant())); + Command matchingCommand = commands.Find(c => c.names.Contains(firstCommand)); if (matchingCommand == null) { //if the command is not defined client-side, we'll relay it anyway because it may be a custom command at the server's side @@ -1236,7 +1246,7 @@ namespace Barotrauma NewMessage("Server command: " + command, Color.Cyan); return; } - else if (GameMain.Client.HasConsoleCommandPermission(splitCommand[0].ToLowerInvariant())) + else if (GameMain.Client.HasConsoleCommandPermission(firstCommand)) { if (matchingCommand.RelayToServer) { @@ -1262,7 +1272,7 @@ namespace Barotrauma bool commandFound = false; foreach (Command c in commands) { - if (!c.names.Contains(splitCommand[0].ToLowerInvariant())) continue; + if (!c.names.Contains(firstCommand)) { continue; } c.Execute(splitCommand.Skip(1).ToArray()); commandFound = true; break; @@ -1273,7 +1283,9 @@ namespace Barotrauma ThrowError("Command \"" + splitCommand[0] + "\" not found."); } } - + + private static string[] ListCharacterNames() => Character.CharacterList.OrderBy(c => c.IsDead).ThenByDescending(c => c.IsHuman).Select(c => c.Name).Distinct().ToArray(); + private static Character FindMatchingCharacter(string[] args, bool ignoreRemotePlayers = false, Client allowedRemotePlayer = null) { if (args.Length == 0) return null; @@ -1290,7 +1302,7 @@ namespace Barotrauma } var matchingCharacters = Character.CharacterList.FindAll(c => - c.Name.ToLowerInvariant() == characterName && + c.Name.Equals(characterName, StringComparison.OrdinalIgnoreCase) && (!c.IsRemotePlayer || !ignoreRemotePlayers || allowedRemotePlayer?.Character == c)); if (!matchingCharacters.Any()) @@ -1336,7 +1348,7 @@ namespace Barotrauma JobPrefab job = null; if (!JobPrefab.Prefabs.ContainsKey(characterLowerCase)) { - job = JobPrefab.Prefabs.Find(jp => jp.Name?.ToLowerInvariant() == characterLowerCase); + job = JobPrefab.Prefabs.Find(jp => jp.Name != null && jp.Name.Equals(characterLowerCase, StringComparison.OrdinalIgnoreCase)); } else { @@ -1594,12 +1606,7 @@ namespace Barotrauma return true; } - public static Command FindCommand(string commandName) - { - commandName = commandName.ToLowerInvariant(); - return commands.Find(c => c.names.Any(n => n.ToLowerInvariant() == commandName)); - } - + public static Command FindCommand(string commandName) => commands.Find(c => c.names.Any(n => n.Equals(commandName, StringComparison.OrdinalIgnoreCase))); public static void Log(string message) { @@ -1620,8 +1627,7 @@ namespace Barotrauma #if CLIENT if (listBox == null) { NewMessage(error, Color.Red); return; } - var textContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), listBox.Content.RectTransform), - style: "InnerFrame", color: Color.White) + var textContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), listBox.Content.RectTransform), style: "InnerFrame", color: Color.White) { CanBeFocused = false }; @@ -1635,7 +1641,7 @@ namespace Barotrauma textBlock.SetTextPos(); listBox.UpdateScrollBarSize(); - listBox.BarScroll = 1.0f; + listBox.BarScroll = 1.0f; if (createMessageBox) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs index 2dd33710c..34ec9fe34 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ArtifactEvent.cs @@ -1,6 +1,4 @@ using Microsoft.Xna.Framework; -using System; -using System.Xml.Linq; namespace Barotrauma { @@ -53,7 +51,7 @@ namespace Barotrauma public override void Init(bool affectSubImmediately) { spawnPos = Level.Loaded.GetRandomItemPos( - (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) < 0.5f) ? Level.PositionType.MainPath : Level.PositionType.Cave | Level.PositionType.Ruin, + (Rand.Value(Rand.RandSync.Server) < 0.5f) ? Level.PositionType.MainPath : Level.PositionType.Cave | Level.PositionType.Ruin, 500.0f, 10000.0f, 30.0f); spawnPending = true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs index 01dd3c557..55a6acc56 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace Barotrauma { @@ -219,13 +218,44 @@ namespace Barotrauma private void CreateEvents(ScriptedEventSet eventSet) { - if (eventSet.ChooseRandom) + int applyCount = 1; + if (eventSet.PerRuin) { - if (eventSet.EventPrefabs.Count > 0) + applyCount = Level.Loaded.Ruins.Count(); + } + else if (eventSet.PerWreck) + { + applyCount = Submarine.Loaded.Count(s => s.Info.IsWreck && (s.WreckAI == null || !s.WreckAI.IsAlive)); + } + for (int i = 0; i < applyCount; i++) + { + if (eventSet.ChooseRandom) { - MTRandom rand = new MTRandom(ToolBox.StringToInt(level.Seed)); - var eventPrefab = ToolBox.SelectWeightedRandom(eventSet.EventPrefabs, eventSet.EventPrefabs.Select(e => e.Commonness).ToList(), rand); - if (eventPrefab != null) + if (eventSet.EventPrefabs.Count > 0) + { + MTRandom rand = new MTRandom(ToolBox.StringToInt(level.Seed)); + var eventPrefab = ToolBox.SelectWeightedRandom(eventSet.EventPrefabs, eventSet.EventPrefabs.Select(e => e.Commonness).ToList(), rand); + if (eventPrefab != null) + { + var newEvent = eventPrefab.CreateInstance(); + newEvent.Init(true); + DebugConsole.Log("Initialized event " + newEvent.ToString()); + if (!selectedEvents.ContainsKey(eventSet)) + { + selectedEvents.Add(eventSet, new List()); + } + selectedEvents[eventSet].Add(newEvent); + } + } + if (eventSet.ChildSets.Count > 0) + { + var newEventSet = SelectRandomEvents(eventSet.ChildSets); + if (newEventSet != null) { CreateEvents(newEventSet); } + } + } + else + { + foreach (ScriptedEventPrefab eventPrefab in eventSet.EventPrefabs) { var newEvent = eventPrefab.CreateInstance(); newEvent.Init(true); @@ -236,30 +266,11 @@ namespace Barotrauma } selectedEvents[eventSet].Add(newEvent); } - } - if (eventSet.ChildSets.Count > 0) - { - var newEventSet = SelectRandomEvents(eventSet.ChildSets); - if (newEventSet != null) { CreateEvents(newEventSet); } - } - } - else - { - foreach (ScriptedEventPrefab eventPrefab in eventSet.EventPrefabs) - { - var newEvent = eventPrefab.CreateInstance(); - newEvent.Init(true); - DebugConsole.Log("Initialized event " + newEvent.ToString()); - if (!selectedEvents.ContainsKey(eventSet)) - { - selectedEvents.Add(eventSet, new List()); - } - selectedEvents[eventSet].Add(newEvent); - } - foreach (ScriptedEventSet childEventSet in eventSet.ChildSets) - { - CreateEvents(childEventSet); + foreach (ScriptedEventSet childEventSet in eventSet.ChildSets) + { + CreateEvents(childEventSet); + } } } } @@ -296,11 +307,14 @@ namespace Barotrauma 0.0f, 1.0f); //don't create new events if within 50 meters of the start/end of the level - if (distanceTraveled <= 0.0f || - distFromStart * Physics.DisplayToRealWorldRatio < 50.0f || - distFromEnd * Physics.DisplayToRealWorldRatio < 50.0f) + if (!eventSet.AllowAtStart) { - return false; + if (distanceTraveled <= 0.0f || + distFromStart * Physics.DisplayToRealWorldRatio < 50.0f || + distFromEnd * Physics.DisplayToRealWorldRatio < 50.0f) + { + return false; + } } if ((Submarine.MainSub == null || distanceTraveled < eventSet.MinDistanceTraveled) && @@ -368,17 +382,15 @@ namespace Barotrauma pendingEventSets.RemoveAt(i); - if (!selectedEvents.ContainsKey(eventSet)) + if (selectedEvents.ContainsKey(eventSet)) { - //no events selected from this event set - continue; + //start events in this set + foreach (ScriptedEvent scriptedEvent in selectedEvents[eventSet]) + { + activeEvents.Add(scriptedEvent); + } } - //start events in this set - foreach (ScriptedEvent scriptedEvent in selectedEvents[eventSet]) - { - activeEvents.Add(scriptedEvent); - } //add child event sets to pending foreach (ScriptedEventSet childEventSet in eventSet.ChildSets) { @@ -431,7 +443,7 @@ namespace Barotrauma enemyDanger = 0.0f; foreach (Character character in Character.CharacterList) { - if (character.IsDead || character.IsUnconscious || !character.Enabled) continue; + if (character.IsDead || character.IsIncapacitated || !character.Enabled) continue; EnemyAIController enemyAI = character.AIController as EnemyAIController; if (enemyAI == null) continue; @@ -458,7 +470,7 @@ namespace Barotrauma int hullCount = 0; foreach (Hull hull in Hull.hullList) { - if (hull.Submarine == null || hull.Submarine.IsOutpost) { continue; } + if (hull.Submarine == null || hull.Submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; } hullCount++; foreach (Gap gap in hull.ConnectedGaps) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs index cb4973991..c5960b7e4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/CargoMission.cs @@ -69,7 +69,7 @@ namespace Barotrauma return; } - WayPoint cargoSpawnPos = WayPoint.GetRandom(SpawnType.Cargo, null, Submarine.MainSub, true); + WayPoint cargoSpawnPos = WayPoint.GetRandom(SpawnType.Cargo, null, Submarine.MainSub, useSyncedRand: true); if (cargoSpawnPos == null) { DebugConsole.ThrowError("Couldn't spawn items for cargo mission, cargo spawnpoint not found"); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs index 12e05cc69..a354e7972 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/MissionPrefab.cs @@ -176,11 +176,11 @@ namespace Barotrauma { foreach (Pair allowedLocationType in AllowedLocationTypes) { - if (allowedLocationType.First.ToLowerInvariant() == "any" || - allowedLocationType.First.ToLowerInvariant() == from.Type.Identifier.ToLowerInvariant()) + if (allowedLocationType.First.Equals("any", StringComparison.OrdinalIgnoreCase) || + allowedLocationType.First.Equals(from.Type.Identifier, StringComparison.OrdinalIgnoreCase)) { - if (allowedLocationType.Second.ToLowerInvariant() == "any" || - allowedLocationType.Second.ToLowerInvariant() == to.Type.Identifier.ToLowerInvariant()) + if (allowedLocationType.Second.Equals("any", StringComparison.OrdinalIgnoreCase) || + allowedLocationType.Second.Equals(to.Type.Identifier, StringComparison.OrdinalIgnoreCase)) { return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs index e3691f34e..2c3333212 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs @@ -1,8 +1,10 @@ -using FarseerPhysics; +using Barotrauma.Extensions; +using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; +using System.Xml.Linq; namespace Barotrauma { @@ -13,6 +15,17 @@ namespace Barotrauma private Item item; private readonly Level.PositionType spawnPositionType; + + private readonly string containerTag; + + private readonly string existingItemTag; + + private readonly bool showMessageWhenPickedUp; + + /// + /// Status effects executed on the target item when the mission starts. A random effect is chosen from each child list. + /// + private readonly List> statusEffects = new List>(); public override IEnumerable SonarPositions { @@ -24,7 +37,7 @@ namespace Barotrauma } else { - yield return ConvertUnits.ToDisplayUnits(item.SimPosition); + yield return item.WorldPosition; } } } @@ -32,6 +45,8 @@ namespace Barotrauma public SalvageMission(MissionPrefab prefab, Location[] locations) : base(prefab, locations) { + containerTag = prefab.ConfigElement.GetAttributeString("containertag", ""); + if (prefab.ConfigElement.Attribute("itemname") != null) { DebugConsole.ThrowError("Error in SalvageMission - use item identifier instead of the name of the item."); @@ -52,32 +67,118 @@ namespace Barotrauma } } + existingItemTag = prefab.ConfigElement.GetAttributeString("existingitemtag", ""); + showMessageWhenPickedUp = prefab.ConfigElement.GetAttributeBool("showmessagewhenpickedup", false); + string spawnPositionTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", ""); if (string.IsNullOrWhiteSpace(spawnPositionTypeStr) || !Enum.TryParse(spawnPositionTypeStr, true, out spawnPositionType)) { spawnPositionType = Level.PositionType.Cave | Level.PositionType.Ruin; } + + foreach (XElement element in prefab.ConfigElement.Elements()) + { + switch (element.Name.ToString().ToLowerInvariant()) + { + case "statuseffect": + { + var newEffect = StatusEffect.Load(element, parentDebugName: prefab.Name); + if (newEffect == null) { continue; } + statusEffects.Add(new List { newEffect }); + break; + } + case "chooserandom": + statusEffects.Add(new List()); + foreach (XElement subElement in element.Elements()) + { + var newEffect = StatusEffect.Load(subElement, parentDebugName: prefab.Name); + if (newEffect == null) { continue; } + statusEffects.Last().Add(newEffect); + } + break; + } + } } public override void Start(Level level) { if (!IsClient) { - //ruin items are allowed to spawn close to the sub - float minDistance = spawnPositionType == Level.PositionType.Ruin ? 0.0f : Level.Loaded.Size.X * 0.3f; + //ruin/wreck items are allowed to spawn close to the sub + float minDistance = spawnPositionType == Level.PositionType.Ruin || spawnPositionType == Level.PositionType.Wreck ? + 0.0f : Level.Loaded.Size.X * 0.3f; Vector2 position = Level.Loaded.GetRandomItemPos(spawnPositionType, 100.0f, minDistance, 30.0f); - item = new Item(itemPrefab, position, null); - item.body.FarseerBody.BodyType = BodyType.Kinematic; - - if (item.HasTag("alien")) + if (!string.IsNullOrEmpty(existingItemTag)) + { + var suitableItems = Item.ItemList.Where(it => it.HasTag(existingItemTag)); + switch (spawnPositionType) + { + case Level.PositionType.Cave: + case Level.PositionType.MainPath: + item = suitableItems.FirstOrDefault(it => Vector2.DistanceSquared(it.WorldPosition, position) < 1000.0f); + break; + case Level.PositionType.Ruin: + item = suitableItems.FirstOrDefault(it => it.ParentRuin != null && it.ParentRuin.Area.Contains(position)); + break; + case Level.PositionType.Wreck: + foreach (Item it in suitableItems) + { + if (it.Submarine == null || it.Submarine.Info.Type != SubmarineInfo.SubmarineType.Wreck) { continue; } + Rectangle worldBorders = it.Submarine.Borders; + worldBorders.Location += it.Submarine.WorldPosition.ToPoint(); + if (Submarine.RectContains(worldBorders, it.WorldPosition)) + { + item = it; +#if SERVER + usedExistingItem = true; +#endif + break; + } + } + break; + } + } + + if (item == null) + { + item = new Item(itemPrefab, position, null); + item.body.FarseerBody.BodyType = BodyType.Kinematic; + item.FindHull(); + } + + for (int i = 0; i < statusEffects.Count; i++) + { + List effectList = statusEffects[i]; + if (effectList.Count == 0) { continue; } + int effectIndex = Rand.Int(effectList.Count); + var selectedEffect = effectList[effectIndex]; + item.ApplyStatusEffect(selectedEffect, selectedEffect.type, deltaTime: 1.0f, worldPosition: item.Position); +#if SERVER + executedEffectIndices.Add(new Pair(i, effectIndex)); +#endif + } + + //try to find a container and place the item inside it + if (!string.IsNullOrEmpty(containerTag) && item.ParentInventory == null) { - //try to find an artifact holder and place the artifact inside it foreach (Item it in Item.ItemList) { - if (it.Submarine != null || !it.HasTag("artifactholder")) continue; - + if (!it.HasTag(containerTag)) { continue; } + switch (spawnPositionType) + { + case Level.PositionType.Cave: + case Level.PositionType.MainPath: + if (it.Submarine != null || it.ParentRuin != null) { continue; } + break; + case Level.PositionType.Ruin: + if (it.ParentRuin == null) { continue; } + break; + case Level.PositionType.Wreck: + if (it.Submarine == null || it.Submarine.Info.Type != SubmarineInfo.SubmarineType.Wreck) { continue; } + break; + } var itemContainer = it.GetComponent(); if (itemContainer == null) { continue; } if (itemContainer.Combine(item, user: null)) { break; } // Placement successful @@ -88,28 +189,46 @@ namespace Barotrauma public override void Update(float deltaTime) { + if (item == null) + { +#if DEBUG + DebugConsole.ThrowError("Error in salvage mission " + Prefab.Identifier + " (item was null)"); +#endif + return; + } + if (IsClient) { - if (item.ParentInventory != null) { item.body.FarseerBody.BodyType = BodyType.Dynamic; } + if (item.ParentInventory != null && item.body != null) { item.body.FarseerBody.BodyType = BodyType.Dynamic; } return; } switch (State) { case 0: - if (item.ParentInventory != null) { item.body.FarseerBody.BodyType = BodyType.Dynamic; } - if (item.CurrentHull?.Submarine == null) { return; } + if (item.ParentInventory != null && item.body != null) { item.body.FarseerBody.BodyType = BodyType.Dynamic; } + if (showMessageWhenPickedUp) + { + if (!(item.ParentInventory?.Owner is Character)) { return; } + } + else + { + if (item.CurrentHull?.Submarine == null || item.CurrentHull.Submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { return; } + } State = 1; break; case 1: if (!Submarine.MainSub.AtEndPosition && !Submarine.MainSub.AtStartPosition) { return; } State = 2; break; - } + } } public override void End() { - if (item.CurrentHull?.Submarine == null || !item.CurrentHull.Submarine.AtEndPosition || item.Removed) { return; } + if (item.CurrentHull?.Submarine == null || (!item.CurrentHull.Submarine.AtEndPosition && !item.CurrentHull.Submarine.AtStartPosition) || item.Removed) + { + return; + } item?.Remove(); item = null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs index df5c497b1..02bdb78d8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/MonsterEvent.cs @@ -1,50 +1,46 @@ using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Xml.Linq; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; namespace Barotrauma { class MonsterEvent : ScriptedEvent { - private string speciesName; - - private int minAmount, maxAmount; - + private readonly string speciesName; + private readonly int minAmount, maxAmount; private List monsters; - private bool spawnDeep; + private readonly bool spawnDeep; private Vector2? spawnPos; - private bool disallowed; - - private Level.PositionType spawnPosType; + private readonly bool disallowed; + + private readonly Level.PositionType spawnPosType; private bool spawnPending; - private string characterFileName; - public override Vector2 DebugDrawPos { - get { return spawnPos.HasValue ? spawnPos.Value : Vector2.Zero; } + get { return spawnPos ?? Vector2.Zero; } } - + public override string ToString() { if (maxAmount <= 1) { - return "MonsterEvent (" + characterFileName + ")"; + return "MonsterEvent (" + speciesName + ")"; } else if (minAmount < maxAmount) { - return "MonsterEvent (" + characterFileName + " x" + minAmount + "-" + maxAmount + ")"; + return "MonsterEvent (" + speciesName + " x" + minAmount + "-" + maxAmount + ")"; } else { - return "MonsterEvent (" + characterFileName + " x" + maxAmount + ")"; + return "MonsterEvent (" + speciesName + " x" + maxAmount + ")"; } } @@ -76,7 +72,6 @@ namespace Barotrauma } spawnDeep = prefab.ConfigElement.GetAttributeBool("spawndeep", false); - characterFileName = Path.GetFileName(Path.GetDirectoryName(speciesName)).ToLower(); if (GameMain.NetworkMember != null) { @@ -85,7 +80,10 @@ namespace Barotrauma if (!string.IsNullOrWhiteSpace(tryKey)) { - if (!GameMain.NetworkMember.ServerSettings.MonsterEnabled[tryKey]) disallowed = true; //spawn was disallowed by host + if (!GameMain.NetworkMember.ServerSettings.MonsterEnabled[tryKey]) + { + disallowed = true; //spawn was disallowed by host + } } } } @@ -106,18 +104,8 @@ namespace Barotrauma public override bool CanAffectSubImmediately(Level level) { - float maxRange = Items.Components.Sonar.DefaultSonarRange * 0.8f; - - List positions = GetAvailableSpawnPositions(); - foreach (Vector2 position in positions) - { - if (Vector2.DistanceSquared(position, Submarine.MainSub.WorldPosition) < maxRange * maxRange) - { - return true; - } - } - - return false; + float maxRange = Sonar.DefaultSonarRange * 0.8f; + return GetAvailableSpawnPositions().Any(p => Vector2.DistanceSquared(p.Position.ToVector2(), Submarine.MainSub.WorldPosition) < maxRange * maxRange); } public override void Init(bool affectSubImmediately) @@ -128,28 +116,44 @@ namespace Barotrauma } } - private List GetAvailableSpawnPositions() + private List GetAvailableSpawnPositions() { - var availablePositions = Level.Loaded.PositionsOfInterest.FindAll(p => spawnPosType.HasFlag(p.PositionType)); - - List positions = new List(); - foreach (var allowedPosition in availablePositions) + var availablePositions = Level.Loaded.PositionsOfInterest.FindAll(p => spawnPosType.HasFlag(p.PositionType) && !Level.Loaded.UsedPositions.Contains(p)); + var removals = new List(); + foreach (var position in availablePositions) { - if (Level.Loaded.ExtraWalls.Any(w => w.Cells.Any(c => c.IsPointInside(allowedPosition.Position.ToVector2())))) { continue; } - positions.Add(allowedPosition.Position.ToVector2()); - } - - if (spawnDeep) - { - for (int i = 0; i < positions.Count; i++) + if (position.Submarine != null) { - positions[i] = new Vector2(positions[i].X, positions[i].Y - Level.Loaded.Size.Y); + if (position.Submarine.WreckAI != null && position.Submarine.WreckAI.IsAlive) + { + removals.Add(position); + } + else + { + continue; + } + } + if (position.PositionType != Level.PositionType.MainPath) { continue; } + if (Level.Loaded.ExtraWalls.Any(w => w.Cells.Any(c => c.IsPointInside(position.Position.ToVector2())))) + { + removals.Add(position); + } + if (spawnDeep) + { + for (int i = 0; i < availablePositions.Count; i++) + { + var pos = availablePositions[i].Position; + pos = new Point(pos.X, pos.Y - Level.Loaded.Size.Y); + availablePositions[i] = new Level.InterestingPosition(pos, availablePositions[i].PositionType); + } + } + if (position.Position.Y < Level.Loaded.GetBottomPosition(position.Position.X).Y) + { + removals.Add(position); } } - - positions.RemoveAll(pos => pos.Y < Level.Loaded.GetBottomPosition(pos.X).Y); - - return positions; + removals.ForEach(r => availablePositions.Remove(r)); + return availablePositions; } private void FindSpawnPosition(bool affectSubImmediately) @@ -158,67 +162,100 @@ namespace Barotrauma spawnPos = Vector2.Zero; var availablePositions = GetAvailableSpawnPositions(); - if (affectSubImmediately && spawnPosType != Level.PositionType.Ruin) + var chosenPosition = new Level.InterestingPosition(Point.Zero, Level.PositionType.MainPath, isValid: false); + var removedPositions = new List(); + foreach (var position in availablePositions) { - if (availablePositions.Count == 0) + if (Rand.Value(Rand.RandSync.Server) > prefab.SpawnProbability) + { + removedPositions.Add(position); + if (prefab.AllowOnlyOnce) + { + Level.Loaded.UsedPositions.Add(position); + } + } + } + removedPositions.ForEach(p => availablePositions.Remove(p)); + bool isSubOrWreck = spawnPosType == Level.PositionType.Ruin || spawnPosType == Level.PositionType.Wreck; + if (affectSubImmediately && !isSubOrWreck) + { + if (availablePositions.None()) { //no suitable position found, disable the event Finished(); return; } - float closestDist = float.PositiveInfinity; //find the closest spawnposition that isn't too close to any of the subs - foreach (Vector2 position in availablePositions) + foreach (var position in availablePositions) { - float dist = Vector2.DistanceSquared(position, Submarine.MainSub.WorldPosition); + Vector2 pos = position.Position.ToVector2(); + float dist = Vector2.DistanceSquared(pos, Submarine.MainSub.WorldPosition); foreach (Submarine sub in Submarine.Loaded) { - if (sub.IsOutpost) { continue; } + if (sub.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; } float minDistToSub = GetMinDistanceToSub(sub); if (dist > minDistToSub * minDistToSub && dist < closestDist) { closestDist = dist; - spawnPos = position; + chosenPosition = position; } } } - //only found a spawnpos that's very far from the sub, pick one that's closer //and wait for the sub to move further before spawning if (closestDist > 15000.0f * 15000.0f) { - foreach (Vector2 position in availablePositions) + foreach (var position in availablePositions) { - float dist = Vector2.DistanceSquared(position, Submarine.MainSub.WorldPosition); + float dist = Vector2.DistanceSquared(position.Position.ToVector2(), Submarine.MainSub.WorldPosition); if (dist < closestDist) { closestDist = dist; - spawnPos = position; + chosenPosition = position; } } } } else { - float minDist = spawnPosType == Level.PositionType.Ruin ? 0.0f : 20000.0f; - availablePositions.RemoveAll(p => Vector2.Distance(Submarine.MainSub.WorldPosition, p) < minDist); - if (availablePositions.Count == 0) + if (!isSubOrWreck) + { + float minDistance = 20000; + availablePositions.RemoveAll(p => Vector2.DistanceSquared(Submarine.MainSub.WorldPosition, p.Position.ToVector2()) < minDistance * minDistance); + } + if (availablePositions.None()) { //no suitable position found, disable the event Finished(); return; } - - spawnPos = availablePositions[Rand.Int(availablePositions.Count, Rand.RandSync.Server)]; + chosenPosition = availablePositions.GetRandom(); + } + if (chosenPosition.IsValid) + { + spawnPos = chosenPosition.Position.ToVector2(); + if (chosenPosition.Submarine != null || chosenPosition.Ruin != null) + { + var spawnPoint = WayPoint.GetRandom(SpawnType.Enemy, sub: chosenPosition.Submarine, ruin: chosenPosition.Ruin, useSyncedRand: false); + if (spawnPoint != null) + { + System.Diagnostics.Debug.Assert(spawnPoint.Submarine == chosenPosition.Submarine); + System.Diagnostics.Debug.Assert(spawnPoint.ParentRuin == chosenPosition.Ruin); + spawnPos = spawnPoint.WorldPosition; + } + } + spawnPending = true; + if (prefab.AllowOnlyOnce) + { + Level.Loaded.UsedPositions.Add(chosenPosition); + } } - spawnPending = true; } private float GetMinDistanceToSub(Submarine submarine) { - //9000 units is slightly less than the default range of the sonar - return Math.Max(Math.Max(submarine.Borders.Width, submarine.Borders.Height), 9000.0f); + return Math.Max(Math.Max(submarine.Borders.Width, submarine.Borders.Height), Sonar.DefaultSonarRange * 0.9f); } public override void Update(float deltaTime) @@ -243,9 +280,38 @@ namespace Barotrauma //wait until there are no submarines at the spawnpos foreach (Submarine submarine in Submarine.Loaded) { - if (submarine.IsOutpost) { continue; } + if (submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; } float minDist = GetMinDistanceToSub(submarine); - if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist) return; + if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist) { return; } + } + + //if spawning in a ruin/cave, wait for someone to be close to it to spawning + //unnecessary monsters in places the players might never visit during the round + if (spawnPosType == Level.PositionType.Ruin || spawnPosType == Level.PositionType.Cave || spawnPosType == Level.PositionType.Wreck) + { + bool someoneNearby = false; + float minDist = Sonar.DefaultSonarRange * 0.8f; + foreach (Submarine submarine in Submarine.Loaded) + { + if (submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; } + if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos.Value) < minDist * minDist) + { + someoneNearby = true; + break; + } + } + foreach (Character c in Character.CharacterList) + { + if (c == Character.Controlled || c.IsRemotePlayer) + { + if (Vector2.DistanceSquared(c.WorldPosition, spawnPos.Value) < minDist * minDist) + { + someoneNearby = true; + break; + } + } + } + if (!someoneNearby) { return; } } spawnPending = false; @@ -280,7 +346,7 @@ namespace Barotrauma Entity targetEntity = Submarine.FindClosest(GameMain.GameScreen.Cam.WorldViewCenter); #if CLIENT - if (Character.Controlled != null) targetEntity = (Entity)Character.Controlled; + if (Character.Controlled != null) { targetEntity = Character.Controlled; } #endif bool monstersDead = true; @@ -297,7 +363,7 @@ namespace Barotrauma } } - if (monstersDead) Finished(); + if (monstersDead) { Finished(); } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs index 58c5889f8..886750df4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEvent.cs @@ -1,6 +1,5 @@ using Microsoft.Xna.Framework; using System.Collections.Generic; -using System.Linq; namespace Barotrauma { @@ -8,7 +7,7 @@ namespace Barotrauma { protected bool isFinished; - private readonly ScriptedEventPrefab prefab; + protected readonly ScriptedEventPrefab prefab; public bool IsFinished { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEventPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEventPrefab.cs index 60e022532..a6dc76b1e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEventPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEventPrefab.cs @@ -7,12 +7,11 @@ namespace Barotrauma { class ScriptedEventPrefab { - public readonly XElement ConfigElement; - - public readonly Type EventType; - + public readonly XElement ConfigElement; + public readonly Type EventType; public readonly string MusicType; - + public readonly float SpawnProbability; + public readonly bool AllowOnlyOnce; public float Commonness; public ScriptedEventPrefab(XElement element) @@ -34,6 +33,8 @@ namespace Barotrauma DebugConsole.ThrowError("Could not find an event class of the type \"" + ConfigElement.Name + "\"."); } Commonness = element.GetAttributeFloat("commonness", 1.0f); + SpawnProbability = Math.Clamp(element.GetAttributeFloat("spawnprobability", 1.0f), 0, 1); + AllowOnlyOnce = element.GetAttributeBool("allowonlyonce", false); } public ScriptedEvent CreateInstance() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEventSet.cs index 333974395..33a357cc0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/ScriptedEventSet.cs @@ -25,6 +25,11 @@ namespace Barotrauma //the events in this set are delayed if the current EventManager intensity is not between these values public readonly float MinIntensity, MaxIntensity; + public readonly bool AllowAtStart; + + public readonly bool PerRuin; + public readonly bool PerWreck; + public readonly Dictionary Commonness; public readonly List EventPrefabs; @@ -54,6 +59,10 @@ namespace Barotrauma MinDistanceTraveled = element.GetAttributeFloat("mindistancetraveled", 0.0f); MinMissionTime = element.GetAttributeFloat("minmissiontime", 0.0f); + AllowAtStart = element.GetAttributeBool("allowatstart", false); + PerRuin = element.GetAttributeBool("perruin", false); + PerWreck = element.GetAttributeBool("perwreck", false); + Commonness[""] = 1.0f; foreach (XElement subElement in element.Elements()) { @@ -63,7 +72,7 @@ namespace Barotrauma Commonness[""] = subElement.GetAttributeFloat("commonness", 0.0f); foreach (XElement overrideElement in subElement.Elements()) { - if (overrideElement.Name.ToString().ToLowerInvariant() == "override") + if (overrideElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { string levelType = overrideElement.GetAttributeString("leveltype", ""); if (!Commonness.ContainsKey(levelType)) @@ -116,7 +125,7 @@ namespace Barotrauma int i = 0; foreach (XElement element in doc.Root.Elements()) { - if (element.Name.ToString().ToLowerInvariant() != "eventset") { continue; } + if (!element.Name.ToString().Equals("eventset", StringComparison.OrdinalIgnoreCase)) { continue; } List.Add(new ScriptedEventSet(element, i.ToString())); i++; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs index 9e129f639..b2dfb78c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/IEnumerableExtensions.cs @@ -7,24 +7,23 @@ namespace Barotrauma.Extensions public static class IEnumerableExtensions { /// - /// Randomizes the collection and returns it. + /// Randomizes the collection (using OrderBy) and returns it. /// - public static IOrderedEnumerable Randomize(this IEnumerable source) + public static IOrderedEnumerable Randomize(this IEnumerable source, Rand.RandSync randSync = Rand.RandSync.Unsynced) { - return source.OrderBy(i => Rand.Value()); + return source.OrderBy(i => Rand.Value(randSync)); } /// - /// Randomizes the list in place. + /// Randomizes the list in place without creating a new collection, using a Fisher-Yates-based algorithm. /// - public static void RandomizeList(this List list) + public static void Shuffle(this IList list, Rand.RandSync randSync = Rand.RandSync.Unsynced) { - //Fisher-Yates shuffle int n = list.Count; while (n > 1) { n--; - int k = Rand.Int(n + 1); + int k = Rand.Int(n + 1, randSync); T value = list[k]; list[k] = list[n]; list[n] = value; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Extensions/RectangleExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Extensions/RectangleExtensions.cs index b76860792..3c221e32e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Extensions/RectangleExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Extensions/RectangleExtensions.cs @@ -57,5 +57,43 @@ namespace Barotrauma.Extensions var size = rect.MultiplySize(scale); return new Rectangle(rect.X, rect.Y, size.X, size.Y); } + + public static bool IntersectsWorld(this Rectangle rect, Rectangle value) + { + int bottom = rect.Y - rect.Height; + int otherBottom = value.Y - value.Height; + return value.Left < rect.Right && rect.Left < value.Right && + value.Top > bottom && rect.Top > otherBottom; + } + + /// + /// Like the XNA method, but treats the y-coordinate so that up is greater and down is lower. + /// + public static bool ContainsWorld(this Rectangle rect, Rectangle other) + { + return + (rect.X <= other.X) && ((other.X + other.Width) <= (rect.X + rect.Width)) && + (rect.Y >= other.Y) && ((other.Y - other.Height) >= (rect.Y - rect.Height)); + } + + /// + /// Like the XNA method, but treats the y-coordinate so that up is greater and down is lower. + /// + public static bool ContainsWorld(this Rectangle rect, Vector2 point) + { + return + (rect.X <= point.X) && (point.X < (rect.X + rect.Width)) && + (rect.Y >= point.Y) && (point.Y > (rect.Y - rect.Height)); + } + + /// + /// Like the XNA method, but treats the y-coordinate so that up is greater and down is lower. + /// + public static bool ContainsWorld(this Rectangle rect, Point point) + { + return + (rect.X <= point.X) && (point.X < (rect.X + rect.Width)) && + (rect.Y >= point.Y) && (point.Y > (rect.Y - rect.Height)); + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/FrameCounter.cs b/Barotrauma/BarotraumaShared/SharedSource/FrameCounter.cs index 647d4c6d8..0b4d5c016 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/FrameCounter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/FrameCounter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; namespace Barotrauma @@ -42,7 +43,7 @@ namespace Barotrauma public float GetAverageElapsedMillisecs(string identifier) { if (!avgTicksPerFrame.ContainsKey(identifier)) return 0.0f; - return avgTicksPerFrame[identifier] / (float)TimeSpan.TicksPerMillisecond; + return avgTicksPerFrame[identifier] * 1000.0f / (float)Stopwatch.Frequency; } public bool Update(double deltaTime) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index b77825408..83825c377 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -23,11 +23,18 @@ namespace Barotrauma { if (Submarine.MainSubs[i] == null) { continue; } List subs = new List() { Submarine.MainSubs[i] }; - subs.AddRange(Submarine.MainSubs[i].DockedTo.Where(d => !d.IsOutpost)); + subs.AddRange(Submarine.MainSubs[i].DockedTo.Where(d => !d.Info.IsOutpost)); Place(subs); } if (campaign != null) { campaign.InitialSuppliesSpawned = true; } - } + } + foreach (var wreck in Submarine.Loaded) + { + if (wreck.Info.IsWreck) + { + Place(wreck.ToEnumerable()); + } + } } private static void Place(IEnumerable subs) @@ -38,10 +45,10 @@ namespace Barotrauma return; } - int sizeApprox = MapEntityPrefab.List.Count() / 3; - var containers = new List(100); - var prefabsWithContainer = new List(sizeApprox / 3); - var prefabsWithoutContainer = new List(sizeApprox); + int itemCountApprox = MapEntityPrefab.List.Count() / 3; + var containers = new List(70 + 30 * subs.Count()); + var prefabsWithContainer = new List(itemCountApprox / 3); + var prefabsWithoutContainer = new List(itemCountApprox); var removals = new List(); foreach (Item item in Item.ItemList) @@ -49,6 +56,7 @@ namespace Barotrauma if (!subs.Contains(item.Submarine)) { continue; } containers.AddRange(item.GetComponents()); } + containers.Shuffle(); foreach (MapEntityPrefab prefab in MapEntityPrefab.List) { @@ -66,7 +74,7 @@ namespace Barotrauma spawnedItems.Clear(); var validContainers = new Dictionary(); - prefabsWithContainer.RandomizeList(); + prefabsWithContainer.Shuffle(); // Spawn items that have an ItemContainer component first so we can fill them up with items if needed (oxygen tanks inside the spawned diving masks, etc) for (int i = 0; i < prefabsWithContainer.Count; i++) { @@ -82,12 +90,13 @@ namespace Barotrauma // Another pass for items with containers because also they can spawn inside other items (like smg magazine) prefabsWithContainer.ForEach(i => SpawnItems(i)); // Spawn items that don't have containers last - prefabsWithoutContainer.RandomizeList(); + prefabsWithoutContainer.Shuffle(); prefabsWithoutContainer.ForEach(i => SpawnItems(i)); if (OutputDebugInfo) { - DebugConsole.NewMessage("Automatically placed items: "); + var subNames = subs.Select(s => s.Info.Name).ToList(); + DebugConsole.NewMessage($"Automatically placed items in { string.Join(", ", subNames) }:"); foreach (string itemName in spawnedItems.Select(it => it.Name).Distinct()) { DebugConsole.NewMessage(" - " + itemName + " x" + spawnedItems.Count(it => it.Name == itemName)); @@ -149,7 +158,12 @@ namespace Barotrauma private static bool SpawnItem(ItemPrefab itemPrefab, List containers, KeyValuePair validContainer) { bool success = false; - if (Rand.Value() > validContainer.Value.SpawnProbability) { return success; } + if (Rand.Value() > validContainer.Value.SpawnProbability) { return false; } + // Don't add dangerously reactive materials in thalamus wrecks + if (validContainer.Key.Item.Submarine.WreckAI != null && itemPrefab.Tags.Contains("explodesinwater")) + { + return false; + } int amount = Rand.Range(validContainer.Value.MinAmount, validContainer.Value.MaxAmount + 1); for (int i = 0; i < amount; i++) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index ad7a1b428..02d14dbb9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -1,4 +1,5 @@ using Barotrauma.Items.Components; +using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -20,8 +21,9 @@ namespace Barotrauma class CargoManager { - private readonly List purchasedItems; + public const int MaxQuantity = 100; + private readonly List purchasedItems; private readonly CampaignMode campaign; public Action OnItemsChanged; @@ -115,9 +117,25 @@ namespace Barotrauma ItemPrefab containerPrefab = null; foreach (PurchasedItem pi in itemsToSpawn) { + float floorPos = cargoRoom.Rect.Y - cargoRoom.Rect.Height; + Vector2 position = new Vector2( Rand.Range(cargoRoom.Rect.X + 20, cargoRoom.Rect.Right - 20), - cargoRoom.Rect.Y - cargoRoom.Rect.Height + pi.ItemPrefab.Size.Y / 2); + floorPos); + + //check where the actual floor structure is in case the bottom of the hull extends below it + if (Submarine.PickBody( + ConvertUnits.ToSimUnits(new Vector2(position.X, cargoRoom.Rect.Y - cargoRoom.Rect.Height / 2)), + ConvertUnits.ToSimUnits(position), + collisionCategory: Physics.CollisionWall) != null) + { + float floorStructurePos = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition.Y); + if (floorStructurePos > floorPos) + { + floorPos = floorStructurePos; + } + } + position.Y = floorPos + pi.ItemPrefab.Size.Y / 2; ItemContainer itemContainer = null; if (!string.IsNullOrEmpty(pi.ItemPrefab.CargoContainerIdentifier)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs index ca5d46580..98b2bf6ff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CrewManager.cs @@ -8,6 +8,7 @@ namespace Barotrauma { const float ConversationIntervalMin = 100.0f; const float ConversationIntervalMax = 180.0f; + const float ConversationIntervalMultiplierMultiplayer = 5.0f; private float conversationTimer, conversationLineTimer; private List> pendingConversationLines = new List>(); @@ -74,11 +75,17 @@ namespace Barotrauma private void UpdateConversations(float deltaTime) { + if (GameMain.NetworkMember != null && GameMain.NetworkMember.ServerSettings.DisableBotConversations) { return; } + conversationTimer -= deltaTime; if (conversationTimer <= 0.0f) { CreateRandomConversation(); conversationTimer = Rand.Range(ConversationIntervalMin, ConversationIntervalMax); + if (GameMain.NetworkMember != null) + { + conversationTimer *= ConversationIntervalMultiplierMultiplayer; + } } if (pendingConversationLines.Count > 0) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index a5ced4b18..f11ef8cb3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -64,7 +64,7 @@ namespace Barotrauma return Submarine.Loaded.FindAll(s => s != leavingSub && !leavingSub.DockedTo.Contains(s) && - s != Level.Loaded.StartOutpost && s != Level.Loaded.EndOutpost && + s.Info.Type == SubmarineInfo.SubmarineType.Player && (s.AtEndPosition != leavingSub.AtEndPosition || s.AtStartPosition != leavingSub.AtStartPosition)); } @@ -80,7 +80,7 @@ namespace Barotrauma { foreach (Structure wall in Structure.WallList) { - if (wall.Submarine == null || wall.Submarine.IsOutpost) { continue; } + if (wall.Submarine == null || wall.Submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; } if (wall.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(wall.Submarine)) { for (int i = 0; i < wall.SectionCount; i++) @@ -95,7 +95,7 @@ namespace Barotrauma { foreach (Item item in Item.ItemList) { - if (item.Submarine == null || item.Submarine.IsOutpost) { continue; } + if (item.Submarine == null || item.Submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; } if (item.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(item.Submarine)) { if (item.GetComponent() != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameModePreset.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameModePreset.cs index bbe5dd8e1..b652d1e8f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameModePreset.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/GameModePreset.cs @@ -44,6 +44,7 @@ namespace Barotrauma { #if CLIENT new GameModePreset("singleplayercampaign", typeof(SinglePlayerCampaign), true); + new GameModePreset("subtest", typeof(SubTestMode), true); new GameModePreset("tutorial", typeof(TutorialMode), true); new GameModePreset("devsandbox", typeof(GameMode), true); #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs index 0bf30613e..32e2756bb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -69,9 +69,18 @@ namespace Barotrauma #if CLIENT if (GameMain.Client != null) { + bool success = + GameMain.Client.ConnectedClients.Any(c => c.Character != null && !c.Character.IsDead); + GameMain.GameSession.EndRound(""); GameMain.GameSession.CrewManager.EndRound(); - return; + + if (success) + { + GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); + } + + return; } #endif @@ -104,17 +113,12 @@ namespace Barotrauma { if (c.Character?.Info != null && !c.Character.IsDead) { + c.Character.ResetCurrentOrder(); c.CharacterInfo = c.Character.Info; characterData.Add(new CharacterCampaignData(c)); } } - //remove all items that are in someone's inventory - foreach (Character c in Character.CharacterList) - { - c.Inventory?.DeleteAllItems(); - } - if (success) { bool atEndPosition = Submarine.MainSub.AtEndPosition; @@ -142,6 +146,8 @@ namespace Barotrauma } map.ProgressWorld(); + GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); + SaveUtil.SaveGame(GameMain.GameSession.SavePath); } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 639dc51d5..f4aedc0da 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -9,7 +9,7 @@ namespace Barotrauma { partial class GameSession { - public enum InfoFrameTab { Crew, Mission, MyCharacter, ManagePlayers }; + public enum InfoFrameTab { Crew, Mission, MyCharacter, Traitor }; public readonly EventManager EventManager; @@ -65,44 +65,51 @@ namespace Barotrauma } } + public SubmarineInfo SubmarineInfo { get; set; } + public Submarine Submarine { get; set; } public string SavePath { get; set; } partial void InitProjSpecific(); - public GameSession(Submarine submarine, string savePath, GameModePreset gameModePreset, MissionType missionType = MissionType.None) - : this(submarine, savePath) + public GameSession(SubmarineInfo submarineInfo, string savePath, GameModePreset gameModePreset, MissionType missionType = MissionType.None) + : this(submarineInfo, savePath) { CrewManager = new CrewManager(gameModePreset != null && gameModePreset.IsSinglePlayer); GameMode = gameModePreset.Instantiate(missionType); } - public GameSession(Submarine submarine, string savePath, GameModePreset gameModePreset, MissionPrefab missionPrefab) - : this(submarine, savePath) + public GameSession(SubmarineInfo submarineInfo, string savePath, GameModePreset gameModePreset, MissionPrefab missionPrefab) + : this(submarineInfo, savePath) { CrewManager = new CrewManager(gameModePreset != null && gameModePreset.IsSinglePlayer); GameMode = gameModePreset.Instantiate(missionPrefab); + +#if CLIENT + if (GameMode is SubTestMode) { EventManager = null; } +#endif } - private GameSession(Submarine submarine, string savePath) + private GameSession(SubmarineInfo submarineInfo, string savePath) { InitProjSpecific(); - Submarine.MainSub = submarine; - this.Submarine = submarine; + SubmarineInfo = submarineInfo; + /*Submarine = new Submarine(submarineInfo); + Submarine.MainSub = Submarine;*/ GameMain.GameSession = this; EventManager = new EventManager(); this.SavePath = savePath; } - public GameSession(Submarine selectedSub, string saveFile, XDocument doc) - : this(selectedSub, saveFile) + public GameSession(SubmarineInfo selectedSubInfo, string saveFile, XDocument doc) + : this(selectedSubInfo, saveFile) { Submarine.MainSub = Submarine; GameMain.GameSession = this; - selectedSub.Name = doc.Root.GetAttributeString("submarine", selectedSub.Name); + //selectedSub.Name = doc.Root.GetAttributeString("submarine", selectedSub.Name); foreach (XElement subElement in doc.Root.Elements()) { @@ -154,10 +161,10 @@ namespace Barotrauma { Level randomLevel = Level.CreateRandom(levelSeed, difficulty); - StartRound(randomLevel, true); + StartRound(randomLevel); } - public void StartRound(Level level, bool reloadSub = true, bool mirrorLevel = false) + public void StartRound(Level level, bool mirrorLevel = false) { //make sure no status effects have been carried on from the next round //(they should be stopped in EndRound, this is a safeguard against cases where the round is ended ungracefully) @@ -169,33 +176,26 @@ namespace Barotrauma #endif this.Level = level; - if (Submarine == null) + if (SubmarineInfo == null) { DebugConsole.ThrowError("Couldn't start game session, submarine not selected."); return; } - if (reloadSub || Submarine.MainSub != Submarine) { Submarine.Load(true); } - Submarine.MainSub = Submarine; - if (GameMode.Mission != null && GameMode.Mission.TeamCount > 1) - { - if (Submarine.MainSubs[1] == null) - { - Submarine.MainSubs[1] = new Submarine(Submarine.MainSub.FilePath, Submarine.MainSub.MD5Hash.Hash, true); - Submarine.MainSubs[1].Load(false); - } - else if (reloadSub) - { - Submarine.MainSubs[1].Load(false); - } - } - - if (Submarine.IsFileCorrupted) + if (SubmarineInfo.IsFileCorrupted) { DebugConsole.ThrowError("Couldn't start game session, submarine file corrupted."); return; } + Submarine.Unload(); + Submarine = Submarine.MainSub = new Submarine(SubmarineInfo); + Submarine.MainSub = Submarine; + if (GameMode.Mission != null && GameMode.Mission.TeamCount > 1 && Submarine.MainSubs[1] == null) + { + Submarine.MainSubs[1] = new Submarine(SubmarineInfo, true); + } + if (level != null) { level.Generate(mirrorLevel); @@ -231,7 +231,7 @@ namespace Barotrauma if (port.Item.WorldPosition.Y < Submarine.WorldPosition.Y) { continue; } float dist = Vector2.DistanceSquared(port.Item.WorldPosition, level.StartOutpost.WorldPosition); - if (myPort == null || dist < closestDistance) + if (myPort == null || dist < closestDistance || (port.MainDockingPort && !myPort.MainDockingPort)) { myPort = port; closestDistance = dist; @@ -254,14 +254,14 @@ namespace Barotrauma foreach (var sub in Submarine.Loaded) { - if (sub.IsOutpost) + if (sub.Info.IsOutpost) { sub.DisableObstructedWayPoints(); } } Entity.Spawner = new EntitySpawner(); - + if (GameMode.Mission != null) { Mission = GameMode.Mission; } if (GameMode != null) { GameMode.Start(); } if (GameMode.Mission != null) @@ -277,7 +277,7 @@ namespace Barotrauma } } - EventManager.StartRound(level); + EventManager?.StartRound(level); SteamAchievementManager.OnStartRound(); if (GameMode != null) @@ -286,8 +286,9 @@ namespace Barotrauma if (GameMain.NetworkMember == null) { - //only autoplace items here in single player + //only place items and corpses here in single player //the server does this after loading the respawn shuttle + Level?.SpawnCorpses(); AutoItemPlacer.PlaceIfNeeded(GameMode); } if (GameMode is MultiPlayerCampaign mpCampaign && GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) @@ -296,18 +297,18 @@ namespace Barotrauma } } - GameAnalyticsManager.AddDesignEvent("Submarine:" + Submarine.Name); - GameAnalyticsManager.AddDesignEvent("Level", ToolBox.StringToInt(level.Seed)); + GameAnalyticsManager.AddDesignEvent("Submarine:" + Submarine.Info.Name); + GameAnalyticsManager.AddDesignEvent("Level", ToolBox.StringToInt(level?.Seed ?? "[NO_LEVEL]")); GameAnalyticsManager.AddProgressionEvent(GameAnalyticsSDK.Net.EGAProgressionStatus.Start, GameMode.Preset.Identifier, (Mission == null ? "None" : Mission.GetType().ToString())); #if CLIENT if (GameMode is SinglePlayerCampaign) { SteamAchievementManager.OnBiomeDiscovered(level.Biome); } - RoundSummary = new RoundSummary(this); + if (!(GameMode is SubTestMode)) { RoundSummary = new RoundSummary(this); } GameMain.GameScreen.ColorFade(Color.Black, Color.TransparentBlack, 5.0f); - if (!(GameMode is TutorialMode)) + if (!(GameMode is TutorialMode) && !(GameMode is SubTestMode)) { GUI.AddMessage("", Color.Transparent, 3.0f, playSound: false); GUI.AddMessage(level.Biome.DisplayName, Color.Lerp(Color.CadetBlue, Color.DarkRed, level.Difficulty / 100.0f), 5.0f, playSound: false); @@ -322,7 +323,7 @@ namespace Barotrauma public void Update(float deltaTime) { - EventManager.Update(deltaTime); + EventManager?.Update(deltaTime); GameMode?.Update(deltaTime); Mission?.Update(deltaTime); @@ -350,9 +351,11 @@ namespace Barotrauma OnClicked = (GUIButton button, object obj) => { GUIMessageBox.MessageBoxes.Remove(summaryFrame); return true; } }; } + + TabMenu.OnRoundEnded(); #endif - EventManager.EndRound(); + EventManager?.EndRound(); SteamAchievementManager.OnRoundEnded(this); Mission = null; @@ -451,7 +454,7 @@ namespace Barotrauma XDocument doc = new XDocument(new XElement("Gamesession")); doc.Root.Add(new XAttribute("savetime", ToolBox.Epoch.NowLocal)); - doc.Root.Add(new XAttribute("submarine", Submarine == null ? "" : Submarine.Name)); + doc.Root.Add(new XAttribute("submarine", SubmarineInfo == null ? "" : SubmarineInfo.Name)); doc.Root.Add(new XAttribute("mapseed", Map.Seed)); doc.Root.Add(new XAttribute("selectedcontentpackages", string.Join("|", GameMain.Config.SelectedContentPackages.Where(cp => cp.HasMultiplayerIncompatibleContent).Select(cp => cp.Path)))); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs index 11424d818..ef8f66979 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSettings.cs @@ -27,10 +27,10 @@ namespace Barotrauma } public partial class GameSettings - { - const string savePath = "config.xml"; - const string playerSavePath = "config_player.xml"; - const string vanillaContentPackagePath = "Data/ContentPackages/Vanilla"; + { + public const string SavePath = "config.xml"; + public const string PlayerSavePath = "config_player.xml"; + public const string VanillaContentPackagePath = "Data/ContentPackages/Vanilla"; public int GraphicsWidth { get; set; } public int GraphicsHeight { get; set; } @@ -91,6 +91,7 @@ namespace Barotrauma set { /*do nothing*/ } } #endif + public bool UseDualModeSockets { get; set; } = true; public bool AutoUpdateWorkshopItems; @@ -117,6 +118,19 @@ namespace Barotrauma set { jobPreferences = value; } } + public bool AreJobPreferencesEqual(List> compareTo) + { + if (jobPreferences == null || compareTo == null) return false; + if (jobPreferences.Count != compareTo.Count) return false; + + for (int i = 0; i < jobPreferences.Count; i++) + { + if (jobPreferences[i].First != compareTo[i].First || jobPreferences[i].Second != compareTo[i].Second) return false; + } + + return true; + } + public int CharacterHeadIndex { get; set; } public int CharacterHairIndex { get; set; } public int CharacterBeardIndex { get; set; } @@ -138,6 +152,13 @@ namespace Barotrauma public bool CrewMenuOpen { get; set; } = true; public bool ChatOpen { get; set; } = true; + public float CorpseDespawnDelay { get; set; } = 10.0f * 60.0f; + + /// + /// How many corpses there can be in a sub before they start to get despawned + /// + public int CorpsesPerSubDespawnThreshold { get; set; } = 5; + private string overrideSaveFolder, overrideMultiplayerSaveFolder; private bool unsavedSettings; @@ -196,13 +217,20 @@ namespace Barotrauma get { return voiceChatVolume; } set { - voiceChatVolume = MathHelper.Clamp(value, 0.0f, 1.0f); + voiceChatVolume = MathHelper.Clamp(value, 0.0f, 2.0f); #if CLIENT - GameMain.SoundManager?.SetCategoryGainMultiplier("voip", voiceChatVolume * 30.0f, 0); + GameMain.SoundManager?.SetCategoryGainMultiplier("voip", Math.Min(voiceChatVolume, 1.0f), 0); #endif } } + + public int VoiceChatCutoffPrevention + { + get; + set; + } + public const float MaxMicrophoneVolume = 10.0f; public float MicrophoneVolume { @@ -236,6 +264,7 @@ namespace Barotrauma #if DEBUG public bool AutomaticQuickStartEnabled { get; set; } + public bool TextManagerDebugModeEnabled { get; set; } #endif private FileSystemWatcher modsFolderWatcher; @@ -311,7 +340,7 @@ namespace Barotrauma ref shouldRefreshAfflictions); if (shouldRefreshAfflictions) { AfflictionPrefab.LoadAll(GameMain.Instance.GetFilesOfType(ContentType.Afflictions)); } - if (shouldRefreshSubs) { Submarine.RefreshSavedSubs(); } + if (shouldRefreshSubs) { SubmarineInfo.RefreshSavedSubs(); } if (shouldRefreshFabricationRecipes) { ItemPrefab.InitFabricationRecipes(); } if (shouldRefreshRuinGenerationParams) { RuinGeneration.RuinGenerationParams.ClearAll(); } if (shouldRefreshScriptedEventSets) { ScriptedEventSet.LoadPrefabs(); } @@ -359,7 +388,7 @@ namespace Barotrauma ref shouldRefreshAfflictions); if (shouldRefreshAfflictions) { AfflictionPrefab.LoadAll(GameMain.Instance.GetFilesOfType(ContentType.Afflictions)); } - if (shouldRefreshSubs) { Submarine.RefreshSavedSubs(); } + if (shouldRefreshSubs) { SubmarineInfo.RefreshSavedSubs(); } if (shouldRefreshFabricationRecipes) { ItemPrefab.InitFabricationRecipes(); } if (shouldRefreshRuinGenerationParams) { RuinGeneration.RuinGenerationParams.ClearAll(); } if (shouldRefreshScriptedEventSets) { ScriptedEventSet.LoadPrefabs(); } @@ -408,7 +437,7 @@ namespace Barotrauma ref shouldRefreshAfflictions); if (shouldRefreshAfflictions) { AfflictionPrefab.LoadAll(GameMain.Instance.GetFilesOfType(ContentType.Afflictions)); } - if (shouldRefreshSubs) { Submarine.RefreshSavedSubs(); } + if (shouldRefreshSubs) { SubmarineInfo.RefreshSavedSubs(); } if (shouldRefreshFabricationRecipes) { ItemPrefab.InitFabricationRecipes(); } if (shouldRefreshRuinGenerationParams) { RuinGeneration.RuinGenerationParams.ClearAll(); } if (shouldRefreshScriptedEventSets) { ScriptedEventSet.LoadPrefabs(); } @@ -496,10 +525,10 @@ namespace Barotrauma shouldRefreshSoundPlayer = true; break; case ContentType.Particles: - GameMain.ParticleManager.LoadPrefabsFromFile(file); + GameMain.ParticleManager?.LoadPrefabsFromFile(file); break; case ContentType.Decals: - GameMain.DecalManager.LoadFromFile(file); + GameMain.DecalManager?.LoadFromFile(file); break; #endif } @@ -579,10 +608,10 @@ namespace Barotrauma shouldRefreshSoundPlayer = true; break; case ContentType.Particles: - GameMain.ParticleManager.RemovePrefabsByFile(file.Path); + GameMain.ParticleManager?.RemovePrefabsByFile(file.Path); break; case ContentType.Decals: - GameMain.DecalManager.RemoveByFile(file.Path); + GameMain.DecalManager?.RemoveByFile(file.Path); break; #endif } @@ -615,6 +644,7 @@ namespace Barotrauma case ContentType.Particles: case ContentType.Decals: case ContentType.Outpost: + case ContentType.Wreck: case ContentType.BackgroundCreaturePrefabs: case ContentType.ServerExecutable: case ContentType.None: @@ -643,7 +673,7 @@ namespace Barotrauma ItemAssemblyPrefab.Prefabs.SortAll(); StructurePrefab.Prefabs.SortAll(); - Submarine.RefreshSavedSubs(); + SubmarineInfo.RefreshSavedSubs(); ItemPrefab.InitFabricationRecipes(); RuinGeneration.RuinGenerationParams.ClearAll(); ScriptedEventSet.LoadPrefabs(); @@ -724,6 +754,10 @@ namespace Barotrauma public bool ShowLanguageSelectionPrompt { get; set; } private bool showTutorialSkipWarning = true; + + public static bool EnableSubmarineAutoSave { get; set; } + public static Color SubEditorBackgroundColor { get; set; } + public bool ShowTutorialSkipWarning { get { return showTutorialSkipWarning && CompletedTutorialNames.Count == 0; } @@ -757,7 +791,7 @@ namespace Barotrauma private void OnModFolderUpdate(object sender, FileSystemEventArgs e) { - if (SuppressModFolderWatcher || !(GameMain.NetworkMember?.IsClient ?? false)) { return; } + if (SuppressModFolderWatcher || (GameMain.NetworkMember?.IsClient ?? false)) { return; } switch (e.ChangeType) { case WatcherChangeTypes.Created: @@ -818,7 +852,7 @@ namespace Barotrauma private void LoadDefaultConfig(bool setLanguage = true) { - XDocument doc = XMLExtensions.TryLoadXml(savePath); + XDocument doc = XMLExtensions.TryLoadXml(SavePath); if (doc == null) { GraphicsWidth = 1024; @@ -875,8 +909,11 @@ namespace Barotrauma new XAttribute("soundvolume", soundVolume), new XAttribute("microphonevolume", microphoneVolume), new XAttribute("voicechatvolume", voiceChatVolume), + new XAttribute("voicechatcutoffprevention", VoiceChatCutoffPrevention), new XAttribute("verboselogging", VerboseLogging), new XAttribute("savedebugconsolelogs", SaveDebugConsoleLogs), + new XAttribute("submarineautosave", EnableSubmarineAutoSave), + new XAttribute("subeditorbackground", XMLExtensions.ColorToString(SubEditorBackgroundColor)), new XAttribute("enablesplashscreen", EnableSplashScreen), new XAttribute("usesteammatchmaking", UseSteamMatchmaking), new XAttribute("quickstartsub", QuickStartSubmarineName), @@ -931,7 +968,7 @@ namespace Barotrauma foreach (ContentPackage contentPackage in SelectedContentPackages) { - if (contentPackage.Path.Contains(vanillaContentPackagePath)) + if (contentPackage.Path.Contains(VanillaContentPackagePath)) { doc.Root.Add(new XElement("contentpackage", new XAttribute("path", contentPackage.Path))); break; @@ -986,7 +1023,7 @@ namespace Barotrauma try { - using (var writer = XmlWriter.Create(savePath, settings)) + using (var writer = XmlWriter.Create(SavePath, settings)) { doc.WriteTo(writer); writer.Flush(); @@ -1021,7 +1058,7 @@ namespace Barotrauma /// private bool LoadPlayerConfigInternal() { - XDocument doc = XMLExtensions.LoadXml(playerSavePath); + XDocument doc = XMLExtensions.LoadXml(PlayerSavePath); if (doc == null || doc.Root == null) { ShowUserStatisticsPrompt = true; @@ -1187,6 +1224,8 @@ namespace Barotrauma new XAttribute("soundvolume", soundVolume), new XAttribute("verboselogging", VerboseLogging), new XAttribute("savedebugconsolelogs", SaveDebugConsoleLogs), + new XAttribute("submarineautosave", EnableSubmarineAutoSave), + new XAttribute("subeditorbackground", XMLExtensions.ColorToString(SubEditorBackgroundColor)), new XAttribute("enablesplashscreen", EnableSplashScreen), new XAttribute("usesteammatchmaking", UseSteamMatchmaking), new XAttribute("quickstartsub", QuickStartSubmarineName), @@ -1199,9 +1238,13 @@ namespace Barotrauma new XAttribute("crewmenuopen", CrewMenuOpen), new XAttribute("campaigndisclaimershown", CampaignDisclaimerShown), new XAttribute("editordisclaimershown", EditorDisclaimerShown), - new XAttribute("tutorialskipwarning", ShowTutorialSkipWarning) + new XAttribute("tutorialskipwarning", ShowTutorialSkipWarning), + new XAttribute("corpsedespawndelay", CorpseDespawnDelay), + new XAttribute("corpsespersubdespawnthreshold", CorpsesPerSubDespawnThreshold), + new XAttribute("usedualmodesockets", UseDualModeSockets) #if DEBUG , new XAttribute("automaticquickstartenabled", AutomaticQuickStartEnabled) + , new XAttribute("textmanagerdebugmodeenabled", TextManagerDebugModeEnabled) #endif ); @@ -1250,6 +1293,7 @@ namespace Barotrauma new XAttribute("musicvolume", musicVolume), new XAttribute("soundvolume", soundVolume), new XAttribute("voicechatvolume", voiceChatVolume), + new XAttribute("voicechatcutoffprevention", VoiceChatCutoffPrevention), new XAttribute("microphonevolume", microphoneVolume), new XAttribute("muteonfocuslost", MuteOnFocusLost), new XAttribute("dynamicrangecompressionenabled", DynamicRangeCompressionEnabled), @@ -1349,7 +1393,7 @@ namespace Barotrauma try { - using (var writer = XmlWriter.Create(playerSavePath, settings)) + using (var writer = XmlWriter.Create(PlayerSavePath, settings)) { doc.WriteTo(writer); writer.Flush(); @@ -1374,6 +1418,8 @@ namespace Barotrauma AutoCheckUpdates = doc.Root.GetAttributeBool("autocheckupdates", AutoCheckUpdates); sendUserStatistics = doc.Root.GetAttributeBool("senduserstatistics", sendUserStatistics); QuickStartSubmarineName = doc.Root.GetAttributeString("quickstartsub", QuickStartSubmarineName); + EnableSubmarineAutoSave = doc.Root.GetAttributeBool("submarineautosave", true); + SubEditorBackgroundColor = doc.Root.GetAttributeColor("subeditorbackground", new Color(0.051f, 0.149f, 0.271f, 1.0f)); UseSteamMatchmaking = doc.Root.GetAttributeBool("usesteammatchmaking", UseSteamMatchmaking); RequireSteamAuthentication = doc.Root.GetAttributeBool("requiresteamauthentication", RequireSteamAuthentication); EnableSplashScreen = doc.Root.GetAttributeBool("enablesplashscreen", EnableSplashScreen); @@ -1382,11 +1428,15 @@ namespace Barotrauma EnableMouseLook = doc.Root.GetAttributeBool("enablemouselook", EnableMouseLook); CrewMenuOpen = doc.Root.GetAttributeBool("crewmenuopen", CrewMenuOpen); ChatOpen = doc.Root.GetAttributeBool("chatopen", ChatOpen); + CorpseDespawnDelay = doc.Root.GetAttributeInt("corpsedespawndelay", 10 * 60); + CorpsesPerSubDespawnThreshold = doc.Root.GetAttributeInt("corpsespersubdespawnthreshold", 5); CampaignDisclaimerShown = doc.Root.GetAttributeBool("campaigndisclaimershown", CampaignDisclaimerShown); EditorDisclaimerShown = doc.Root.GetAttributeBool("editordisclaimershown", EditorDisclaimerShown); ShowTutorialSkipWarning = doc.Root.GetAttributeBool("tutorialskipwarning", true); + UseDualModeSockets = doc.Root.GetAttributeBool("usedualmodesockets", true); #if DEBUG AutomaticQuickStartEnabled = doc.Root.GetAttributeBool("automaticquickstartenabled", AutomaticQuickStartEnabled); + TextManagerDebugModeEnabled = doc.Root.GetAttributeBool("textmanagerdebugmodeenabled", TextManagerDebugModeEnabled); #endif XElement gameplayElement = doc.Root.Element("gameplay"); jobPreferences = new List>(); @@ -1472,6 +1522,7 @@ namespace Barotrauma DynamicRangeCompressionEnabled = audioSettings.GetAttributeBool("dynamicrangecompressionenabled", DynamicRangeCompressionEnabled); VoipAttenuationEnabled = audioSettings.GetAttributeBool("voipattenuationenabled", VoipAttenuationEnabled); VoiceChatVolume = audioSettings.GetAttributeFloat("voicechatvolume", VoiceChatVolume); + VoiceChatCutoffPrevention = audioSettings.GetAttributeInt("voicechatcutoffprevention", VoiceChatCutoffPrevention); MuteOnFocusLost = audioSettings.GetAttributeBool("muteonfocuslost", MuteOnFocusLost); UseDirectionalVoiceChat = audioSettings.GetAttributeBool("usedirectionalvoicechat", UseDirectionalVoiceChat); @@ -1560,6 +1611,8 @@ namespace Barotrauma InventoryScale = 1; AutoUpdateWorkshopItems = true; CampaignDisclaimerShown = false; + CorpseDespawnDelay = 10 * 60; + CorpsesPerSubDespawnThreshold = 5; if (resetLanguage) { Language = "English"; diff --git a/Barotrauma/BarotraumaShared/SharedSource/InputType.cs b/Barotrauma/BarotraumaShared/SharedSource/InputType.cs index addbfee1c..b3ae7c280 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/InputType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/InputType.cs @@ -13,6 +13,7 @@ namespace Barotrauma SelectNextCharacter, SelectPreviousCharacter, Voice, + LocalVoice, Deselect, Shoot, Command, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs index c6cc4d6d8..e962781f8 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/CharacterInventory.cs @@ -73,7 +73,7 @@ namespace Barotrauma foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "item") continue; + if (!subElement.Name.ToString().Equals("item", StringComparison.OrdinalIgnoreCase)) { continue; } string itemIdentifier = subElement.GetAttributeString("identifier", ""); ItemPrefab itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index 5d8abdb7f..181429589 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -58,6 +58,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, false, description: "If set to true, this docking port is used when spawning the submarine docked to an outpost (if possible).")] + public bool MainDockingPort + { + get; + set; + } + public DockingPort DockingTarget { get; private set; } public bool Docked @@ -234,12 +241,12 @@ namespace Barotrauma.Items.Components Vector2 jointDiff = joint.WorldAnchorB - joint.WorldAnchorA; if (item.Submarine.PhysicsBody.Mass < DockingTarget.item.Submarine.PhysicsBody.Mass || - DockingTarget.item.Submarine.IsOutpost) + DockingTarget.item.Submarine.Info.IsOutpost) { item.Submarine.SubBody.SetPosition(item.Submarine.SubBody.Position + ConvertUnits.ToDisplayUnits(jointDiff)); } else if (DockingTarget.item.Submarine.PhysicsBody.Mass < item.Submarine.PhysicsBody.Mass || - item.Submarine.IsOutpost) + item.Submarine.Info.IsOutpost) { DockingTarget.item.Submarine.SubBody.SetPosition(DockingTarget.item.Submarine.SubBody.Position - ConvertUnits.ToDisplayUnits(jointDiff)); } @@ -873,12 +880,12 @@ namespace Barotrauma.Items.Components float closestDist = 30.0f * 30.0f; foreach (Item it in Item.ItemList) { - if (it.Submarine != item.Submarine) continue; + if (it.Submarine != item.Submarine) { continue; } var doorComponent = it.GetComponent(); - if (doorComponent == null) continue; + if (doorComponent == null || doorComponent.IsHorizontal == IsHorizontal) { continue; } - float distSqr = Vector2.Distance(item.Position, it.Position); + float distSqr = Vector2.DistanceSquared(item.Position, it.Position); if (distSqr < closestDist) { door = doorComponent; @@ -951,12 +958,12 @@ namespace Barotrauma.Items.Components if (docked) { if (item.Submarine != null && DockingTarget?.item?.Submarine != null) - GameServer.Log(sender.LogName + " docked " + item.Submarine.Name + " to " + DockingTarget.item.Submarine.Name, ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(sender) + " docked " + item.Submarine.Info.Name + " to " + DockingTarget.item.Submarine.Info.Name, ServerLog.MessageType.ItemInteraction); } else { if (item.Submarine != null && prevDockingTarget?.item?.Submarine != null) - GameServer.Log(sender.LogName + " undocked " + item.Submarine.Name + " from " + prevDockingTarget.item.Submarine.Name, ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(sender) + " undocked " + item.Submarine.Info.Name + " from " + prevDockingTarget.item.Submarine.Info.Name, ServerLog.MessageType.ItemInteraction); } } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs index 45d565f4c..98aa09280 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs @@ -594,7 +594,7 @@ namespace Barotrauma.Items.Components #if SERVER if (sender != null && wasOpen != isOpen) { - GameServer.Log(sender.LogName + (isOpen ? " opened " : " closed ") + item.Name, ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(sender) + (isOpen ? " opened " : " closed ") + item.Name, ServerLog.MessageType.ItemInteraction); } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index 6e9d8981d..25935b392 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -344,7 +344,7 @@ namespace Barotrauma.Items.Components IsActive = true; #if SERVER - if (!alreadyEquipped) GameServer.Log(character.LogName + " equipped " + item.Name, ServerLog.MessageType.ItemInteraction); + if (!alreadyEquipped) GameServer.Log(GameServer.CharacterLogName(character) + " equipped " + item.Name, ServerLog.MessageType.ItemInteraction); #endif } } @@ -355,7 +355,7 @@ namespace Barotrauma.Items.Components picker.DeselectItem(item); #if SERVER - GameServer.Log(character.LogName + " unequipped " + item.Name, ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(character) + " unequipped " + item.Name, ServerLog.MessageType.ItemInteraction); #endif item.body.PhysEnabled = true; @@ -419,7 +419,7 @@ namespace Barotrauma.Items.Components item.CreateServerEvent(this); if (picker != null) { - GameServer.Log(picker.LogName + " detached " + item.Name + " from a wall", ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(picker) + " detached " + item.Name + " from a wall", ServerLog.MessageType.ItemInteraction); } } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index f2a6bf1f6..76a2db767 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -1,4 +1,5 @@ -using FarseerPhysics; +using Barotrauma.Networking; +using FarseerPhysics; using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics.Contacts; using Microsoft.Xna.Framework; @@ -54,7 +55,7 @@ namespace Barotrauma.Items.Components { foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "attack") { continue; } + if (!subElement.Name.ToString().Equals("attack", StringComparison.OrdinalIgnoreCase)) { continue; } attack = new Attack(subElement, item.Name + ", MeleeWeapon"); } item.IsShootable = true; @@ -310,70 +311,7 @@ namespace Barotrauma.Items.Components return false; } - if (attack != null) - { - if (targetLimb == null && targetCharacter == null && targetStructure == null && (targetItem == null || ! targetItem.Prefab.DamagedByMeleeWeapons)) - { - return false; - } - - if (targetLimb != null) - { - targetLimb.character.LastDamageSource = item; - attack.DoDamageToLimb(User, targetLimb, item.WorldPosition, 1.0f); - } - else if (targetCharacter != null) - { - targetCharacter.LastDamageSource = item; - attack.DoDamage(User, targetCharacter, item.WorldPosition, 1.0f); - } - else if (targetStructure != null) - { - attack.DoDamage(User, targetStructure, item.WorldPosition, 1.0f); - } - else if (targetItem != null && targetItem.Prefab.DamagedByMeleeWeapons) - { - attack.DoDamage(User, targetItem, item.WorldPosition, 1.0f); - } - else - { - return false; - } - } - - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return true; } - -#if SERVER - if (GameMain.Server != null && targetCharacter != null) //TODO: Log structure hits - { - - GameMain.Server.CreateEntityEvent(item, new object[] - { - Networking.NetEntityEvent.Type.ApplyStatusEffect, - ActionType.OnUse, - null, //itemcomponent - targetCharacter.ID, targetLimb - }); - - string logStr = picker?.LogName + " used " + item.Name; - if (item.ContainedItems != null && item.ContainedItems.Any()) - { - logStr += " (" + string.Join(", ", item.ContainedItems.Select(i => i?.Name)) + ")"; - } - logStr += " on " + targetCharacter.LogName + "."; - Networking.GameServer.Log(logStr, Networking.ServerLog.MessageType.Attack); - } -#endif - - if (targetCharacter != null) //TODO: Allow OnUse to happen on structures too maybe?? - { - ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, user: User); - } - - if (DeleteOnUse) - { - Entity.Spawner.AddToRemoveQueue(item); - } + impactQueue.Enqueue(f2); return true; } @@ -414,7 +352,7 @@ namespace Barotrauma.Items.Components if (targetStructure.Removed) { return; } attack.DoDamage(User, targetStructure, item.WorldPosition, 1.0f); } - else if (targetItem != null && targetItem.Prefab.DamagedByMeleeWeapons) + else if (targetItem != null && targetItem.Prefab.DamagedByMeleeWeapons && targetItem.Condition > 0) { if (targetItem.Removed) { return; } attack.DoDamage(User, targetItem, item.WorldPosition, 1.0f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs index 94f4d3e38..aa299a0a0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs @@ -29,6 +29,13 @@ namespace Barotrauma.Items.Components set { reload = Math.Max(value, 0.0f); } } + [Serialize(1, false, description: "How projectiles the weapon launches when fired once.")] + public int ProjectileCount + { + get; + set; + } + [Serialize(0.0f, false, description: "Random spread applied to the firing angle of the projectiles when used by a character with sufficient skills to use the weapon (in degrees).")] public float Spread { @@ -110,55 +117,62 @@ namespace Barotrauma.Items.Components ApplyStatusEffects(ActionType.OnFailure, 1.0f, character); } - Projectile projectile = FindProjectile(triggerOnUseOnContainers: true); - if (projectile == null) { return true; } - - float spread = GetSpread(character); - float rotation = (item.body.Dir == 1.0f) ? item.body.Rotation : item.body.Rotation - MathHelper.Pi; - rotation += spread * Rand.Range(-0.5f, 0.5f); - - projectile.User = character; - //add the limbs of the shooter to the list of bodies to be ignored - //so that the player can't shoot himself - projectile.IgnoredBodies = new List(limbBodies); - - Vector2 projectilePos = item.SimPosition; - Vector2 sourcePos = character?.AnimController == null ? item.SimPosition : character.AnimController.AimSourceSimPos; - Vector2 barrelPos = TransformedBarrelPos + item.body.SimPosition; - //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(sourcePos, barrelPos, projectile.IgnoredBodies, Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking) == null) + for (int i = 0; i < ProjectileCount; i++) { - //no obstacles -> we can spawn the projectile at the barrel - projectilePos = barrelPos; + Projectile projectile = FindProjectile(triggerOnUseOnContainers: true); + if (projectile == null) { return true; } + + float spread = GetSpread(character); + float rotation = (item.body.Dir == 1.0f) ? item.body.Rotation : item.body.Rotation - MathHelper.Pi; + rotation += spread * Rand.Range(-0.5f, 0.5f); + + projectile.User = character; + //add the limbs of the shooter to the list of bodies to be ignored + //so that the player can't shoot himself + projectile.IgnoredBodies = new List(limbBodies); + + Vector2 projectilePos = item.SimPosition; + Vector2 sourcePos = character?.AnimController == null ? item.SimPosition : character.AnimController.AimSourceSimPos; + Vector2 barrelPos = TransformedBarrelPos + item.body.SimPosition; + //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(sourcePos, barrelPos, projectile.IgnoredBodies, Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking) == null) + { + //no obstacles -> we can spawn the projectile at the barrel + projectilePos = barrelPos; + } + else if ((sourcePos - barrelPos).LengthSquared() > 0.0001f) + { + //spawn the projectile body.GetMaxExtent() away from the position where the raycast hit the obstacle + projectilePos = sourcePos - Vector2.Normalize(barrelPos - projectilePos) * Math.Max(projectile.Item.body.GetMaxExtent(), 0.1f); + } + + projectile.Item.body.ResetDynamics(); + projectile.Item.SetTransform(projectilePos, rotation); + + projectile.Use(deltaTime); + projectile.Item.GetComponent()?.Attach(item, projectile.Item); + if (projectile.Item.Removed) { continue; } + projectile.User = character; + + projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * Rand.Range(-10.0f, 10.0f)); + + //set the rotation of the projectile again because dropping the projectile resets the rotation + projectile.Item.SetTransform(projectilePos, + rotation + (projectile.Item.body.Dir * projectile.LaunchRotationRadians)); + + item.RemoveContained(projectile.Item); + + if (i == 0) + { + //recoil + item.body.ApplyLinearImpulse( + new Vector2((float)Math.Cos(projectile.Item.body.Rotation), (float)Math.Sin(projectile.Item.body.Rotation)) * item.body.Mass * -50.0f, + maxVelocity: NetConfig.MaxPhysicsBodyVelocity); + } } - else if ((sourcePos - barrelPos).LengthSquared() > 0.0001f) - { - //spawn the projectile body.GetMaxExtent() away from the position where the raycast hit the obstacle - projectilePos = sourcePos - Vector2.Normalize(barrelPos - projectilePos) * Math.Max(projectile.Item.body.GetMaxExtent(), 0.1f); - } - - projectile.Item.body.ResetDynamics(); - projectile.Item.SetTransform(projectilePos, rotation); - - projectile.Use(deltaTime); - if (projectile.Item.Removed) { return true; } - projectile.User = character; - - projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * Rand.Range(-10.0f, 10.0f)); - - //set the rotation of the projectile again because dropping the projectile resets the rotation - projectile.Item.SetTransform(projectilePos, - rotation + (projectile.Item.body.Dir * projectile.LaunchRotationRadians)); - - //recoil - item.body.ApplyLinearImpulse( - new Vector2((float)Math.Cos(projectile.Item.body.Rotation), (float)Math.Sin(projectile.Item.body.Rotation)) * item.body.Mass * -50.0f, - maxVelocity: NetConfig.MaxPhysicsBodyVelocity); LaunchProjSpecific(); - item.RemoveContained(projectile.Item); - return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index 7d7638c33..74cdf27b1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -440,17 +440,7 @@ namespace Barotrauma.Items.Components } else if (targetBody.UserData is Item targetItem) { - targetItem.IsHighlighted = true; - ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, targetItem.AllPropertyObjects); - - if (targetItem.body != null && !MathUtils.NearlyEqual(TargetForce, 0.0f)) - { - Vector2 dir = targetItem.WorldPosition - item.WorldPosition; - dir = dir.LengthSquared() < 0.0001f ? Vector2.UnitY : Vector2.Normalize(dir); - targetItem.body.ApplyForce(dir * TargetForce, maxVelocity: 10.0f); - } - var levelResource = targetItem.GetComponent(); if (levelResource != null && levelResource.Attached && levelResource.requiredItems.Any() && @@ -464,7 +454,22 @@ namespace Barotrauma.Items.Components levelResource.DeattachTimer / levelResource.DeattachDuration, GUI.Style.Red, GUI.Style.Green); #endif + return true; } + + if (!targetItem.Prefab.DamagedByRepairTools) { return false; } + + targetItem.IsHighlighted = true; + + ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, targetItem.AllPropertyObjects); + + if (targetItem.body != null && !MathUtils.NearlyEqual(TargetForce, 0.0f)) + { + Vector2 dir = targetItem.WorldPosition - item.WorldPosition; + dir = dir.LengthSquared() < 0.0001f ? Vector2.UnitY : Vector2.Normalize(dir); + targetItem.body.ApplyForce(dir * TargetForce, maxVelocity: 10.0f); + } + FixItemProjSpecific(user, deltaTime, targetItem); return true; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs index 336303f42..b50e7c525 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Throwable.cs @@ -76,7 +76,7 @@ namespace Barotrauma.Items.Components if (!picker.IsKeyDown(InputType.Aim) && !throwing) { throwPos = 0.0f; } bool aim = picker.IsKeyDown(InputType.Aim) && (picker.SelectedConstruction == null || picker.SelectedConstruction.GetComponent() != null); - if (picker.IsUnconscious || picker.IsDead || !picker.AllowInput) + if (picker.IsDead || !picker.AllowInput) { throwing = false; aim = false; @@ -115,7 +115,7 @@ namespace Barotrauma.Items.Components if (!MathUtils.IsValid(throwVector)) { throwVector = Vector2.UnitY; } #if SERVER - GameServer.Log(picker.LogName + " threw " + item.Name, ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(picker) + " threw " + item.Name, ServerLog.MessageType.ItemInteraction); #endif Character thrower = picker; item.Drop(thrower, createNetworkEvent: GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index 78a17680b..8faa8e1b4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -68,7 +68,6 @@ namespace Barotrauma.Items.Components protected const float CorrectionDelay = 1.0f; protected CoroutineHandle delayedCorrectionCoroutine; - protected float correctionTimer; [Editable, Serialize(0.0f, false, description: "How long it takes to pick up the item (in seconds).")] public float PickingTime @@ -81,7 +80,6 @@ namespace Barotrauma.Items.Components public Action OnActiveStateChanged; - public float IsActiveTimer; public virtual bool IsActive { get { return isActive; } @@ -222,7 +220,6 @@ namespace Barotrauma.Items.Components set; } - /// /// How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not enforced). /// @@ -632,7 +629,7 @@ namespace Barotrauma.Items.Components } /// - /// Only checks the id card(s). Much simpler and a bit different than HasRequiredItems. + /// Only checks if any of the Picked requirements are matched (used for checking id card(s)). Much simpler and a bit different than HasRequiredItems. /// public bool HasAccess(Character character) { @@ -641,7 +638,7 @@ namespace Barotrauma.Items.Components foreach (Item item in character.Inventory.Items) { - if (item?.Prefab.Identifier == "idcard" && requiredItems.Any(ri => ri.Value.Any(r => r.MatchesItem(item)))) + if (requiredItems.Any(ri => ri.Value.Any(r => r.Type == RelatedItem.RelationType.Picked && r.MatchesItem(item)))) { return true; } @@ -741,14 +738,14 @@ namespace Barotrauma.Items.Components { foreach (XAttribute attribute in componentElement.Attributes()) { - if (!SerializableProperties.TryGetValue(attribute.Name.ToString().ToLowerInvariant(), out SerializableProperty property)) continue; + if (!SerializableProperties.TryGetValue(attribute.Name.ToString().ToLowerInvariant(), out SerializableProperty property)) { continue; } property.TrySetValue(this, attribute.Value); } ParseMsg(); OverrideRequiredItems(componentElement); } - if (item.Submarine != null) { SerializableProperty.UpgradeGameVersion(this, originalElement, item.Submarine.GameVersion); } + if (item.Submarine != null) { SerializableProperty.UpgradeGameVersion(this, originalElement, item.Submarine.Info.GameVersion); } } /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs index b9b51b551..4bea6fd32 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs @@ -52,6 +52,9 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, false)] + public bool AccessOnlyWhenBroken { get; set; } + [Serialize(5, false, description: "How many inventory slots the inventory has per row.")] public int SlotsPerRow { get; set; } @@ -197,10 +200,21 @@ namespace Barotrauma.Items.Components } } + public override bool HasRequiredItems(Character character, bool addMessage, string msg = null) + { + return (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg); + } + public override bool Select(Character character) { if (item.Container != null) { return false; } - + if (AccessOnlyWhenBroken) + { + if (item.Condition > 0) + { + return false; + } + } if (AutoInteractWithContained && character.SelectedConstruction == null) { foreach (Item contained in Inventory.Items) @@ -218,6 +232,13 @@ namespace Barotrauma.Items.Components public override bool Pick(Character picker) { + if (AccessOnlyWhenBroken) + { + if (item.Condition > 0) + { + return false; + } + } if (AutoInteractWithContained) { foreach (Item contained in Inventory.Items) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs index 23931bb20..ab5aa5c90 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Xml.Linq; +using Barotrauma.Extensions; namespace Barotrauma.Items.Components { @@ -209,7 +210,7 @@ namespace Barotrauma.Items.Components return true; } - + public override bool SecondaryUse(float deltaTime, Character character = null) { if (this.user != character) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index 424665d8a..ec7020565 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -200,7 +200,7 @@ namespace Barotrauma.Items.Components #if SERVER if (user != null) { - GameServer.Log(user.LogName + (IsActive ? " activated " : " deactivated ") + item.Name, ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(user) + (IsActive ? " activated " : " deactivated ") + item.Name, ServerLog.MessageType.ItemInteraction); } #endif diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs index 3f98ac709..df57597b1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs @@ -55,6 +55,15 @@ namespace Barotrauma.Items.Components get { return Math.Abs((force / 100.0f) * (MinVoltage <= 0.0f ? 1.0f : Math.Min(prevVoltage / MinVoltage, 1.0f))); } } + public float CurrentBrokenVolume + { + get + { + if (item.ConditionPercentage > 10.0f) { return 0.0f; } + return Math.Abs(targetForce / 100.0f) * (1.0f - item.ConditionPercentage / 10.0f); + } + } + public Engine(Item item, XElement element) : base(item, element) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 20ad18b85..746bca34a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -63,7 +63,7 @@ namespace Barotrauma.Items.Components { foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() == "fabricableitem") + if (subElement.Name.ToString().Equals("fabricableitem", StringComparison.OrdinalIgnoreCase)) { DebugConsole.ThrowError("Error in item " + item.Name + "! Fabrication recipes should be defined in the craftable item's xml, not in the fabricator."); break; @@ -180,7 +180,7 @@ namespace Barotrauma.Items.Components #if SERVER if (user != null) { - GameServer.Log(user.LogName + " started fabricating " + selectedItem.DisplayName + " in " + item.Name, ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(user) + " started fabricating " + selectedItem.DisplayName + " in " + item.Name, ServerLog.MessageType.ItemInteraction); } #endif } @@ -216,7 +216,7 @@ namespace Barotrauma.Items.Components #if SERVER if (user != null) { - GameServer.Log(user.LogName + " cancelled the fabrication of " + fabricatedItem.DisplayName + " in " + item.Name, ServerLog.MessageType.ItemInteraction); + GameServer.Log(GameServer.CharacterLogName(user) + " cancelled the fabrication of " + fabricatedItem.DisplayName + " in " + item.Name, ServerLog.MessageType.ItemInteraction); } #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index e1754b5f8..fa2c78107 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -64,11 +64,6 @@ namespace Barotrauma.Items.Components float hullPercentage = 0.0f; if (item.CurrentHull != null) { hullPercentage = (item.CurrentHull.WaterVolume / item.CurrentHull.Volume) * 100.0f; } FlowPercentage = ((float)targetLevel - hullPercentage) * 10.0f; - - if (pumpSpeedLockTimer <= 0.0f) - { - targetLevel = null; - } } currPowerConsumption = powerConsumption * Math.Abs(flowPercentage / 100.0f); @@ -112,6 +107,7 @@ namespace Barotrauma.Items.Components if (float.TryParse(signal, NumberStyles.Any, CultureInfo.InvariantCulture, out float tempSpeed)) { flowPercentage = MathHelper.Clamp(tempSpeed, -100.0f, 100.0f); + targetLevel = null; pumpSpeedLockTimer = 0.1f; } } @@ -131,7 +127,7 @@ namespace Barotrauma.Items.Components if (GameMain.Client != null) { return false; } #endif - if (objective.Option.ToLowerInvariant() == "stoppumping") + if (objective.Option.Equals("stoppumping", StringComparison.OrdinalIgnoreCase)) { #if SERVER if (FlowPercentage > 0.0f) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs index 68d569275..e758ee3e4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using Barotrauma.Extensions; +using System.Globalization; namespace Barotrauma.Items.Components { @@ -189,7 +190,7 @@ namespace Barotrauma.Items.Components { if (Timing.TotalTime >= (float)nextServerLogWriteTime) { - GameServer.Log(lastUser.LogName + " adjusted reactor settings: " + + GameServer.Log(GameServer.CharacterLogName(lastUser) + " adjusted reactor settings: " + "Temperature: " + (int)(temperature * 100.0f) + ", Fission rate: " + (int)targetFissionRate + ", Turbine output: " + (int)targetTurbineOutput + @@ -651,6 +652,20 @@ namespace Barotrauma.Items.Components unsentChanges = true; } break; + case "set_fissionrate": + if (float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float newFissionRate)) + { + FissionRate = newFissionRate; + unsentChanges = true; + } + break; + case "set_turbineoutput": + if (float.TryParse(signal, NumberStyles.Float, CultureInfo.InvariantCulture, out float newTurbineOutput)) + { + TurbineOutput = newTurbineOutput; + unsentChanges = true; + } + break; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs index e514c6a91..2fb664778 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Steering.cs @@ -165,7 +165,6 @@ namespace Barotrauma.Items.Components #region Docking public List DockingSources = new List(); - public DockingPort ActiveDockingSource, DockingTarget; private bool searchedConnectedDockingPort; private bool dockingModeEnabled; @@ -200,7 +199,7 @@ namespace Barotrauma.Items.Components if (dockingConnection != null) { var connectedPorts = item.GetConnectedComponentsRecursive(dockingConnection); - DockingSources.AddRange(connectedPorts.Where(p => p.Item.Submarine != null && !p.Item.Submarine.IsOutpost)); + DockingSources.AddRange(connectedPorts.Where(p => p.Item.Submarine != null && !p.Item.Submarine.Info.IsOutpost)); } } #endregion @@ -344,6 +343,7 @@ namespace Barotrauma.Items.Components autopilotRecalculatePathTimer = RecalculatePathInterval; } + if (steeringPath == null) { return; } steeringPath.CheckProgress(ConvertUnits.ToSimUnits(controlledSub.WorldPosition), 10.0f); if (autopilotRayCastTimer <= 0.0f && steeringPath.NextNode != null) @@ -475,6 +475,8 @@ namespace Barotrauma.Items.Components private void UpdatePath() { + if (Level.Loaded == null) { return; } + if (pathFinder == null) pathFinder = new PathFinder(WayPoint.WayPointList, false); Vector2 target; @@ -536,7 +538,6 @@ namespace Barotrauma.Items.Components } } - private bool aiDockingToggled; public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { if (objective.Override) @@ -572,7 +573,7 @@ namespace Barotrauma.Items.Components } break; case "navigateback": - if (!aiDockingToggled && DockingSources.Any(d => d.Docked)) + if (DockingSources.Any(d => d.Docked)) { item.SendSignal(0, "1", "toggle_docking", sender: null); } @@ -586,7 +587,7 @@ namespace Barotrauma.Items.Components } break; case "navigatetodestination": - if (!aiDockingToggled && DockingSources.Any(d => d.Docked)) + if (DockingSources.Any(d => d.Docked)) { item.SendSignal(0, "1", "toggle_docking", sender: null); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs index f2d9706b1..755391dc9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs @@ -213,7 +213,7 @@ namespace Barotrauma.Items.Components } if (HasBeenTuned) { return true; } - if (string.IsNullOrEmpty(objective.Option) || objective.Option.ToLowerInvariant() == "charge") + if (string.IsNullOrEmpty(objective.Option) || objective.Option.Equals("charge", StringComparison.OrdinalIgnoreCase)) { if (Math.Abs(rechargeSpeed - maxRechargeSpeed * aiRechargeTargetRatio) > 0.05f) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs index 1049831b0..319a44aa6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/Powered.cs @@ -140,6 +140,7 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { + currPowerConsumption = powerConsumption; UpdateOnActiveEffects(deltaTime); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index a7cb03f87..dce150ab1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -11,7 +11,7 @@ using System.Xml.Linq; namespace Barotrauma.Items.Components { - class Projectile : ItemComponent + partial class Projectile : ItemComponent, IServerSerializable { struct HitscanResult { @@ -32,11 +32,13 @@ namespace Barotrauma.Items.Components { public Fixture Fixture; public Vector2 Normal; + public Vector2 LinearVelocity; - public Impact(Fixture fixture, Vector2 normal) + public Impact(Fixture fixture, Vector2 normal, Vector2 velocity) { Fixture = fixture; Normal = normal; + LinearVelocity = velocity; } } @@ -47,16 +49,14 @@ namespace Barotrauma.Items.Components //a duration during which the projectile won't drop from the body it's stuck to private const float PersistentStickJointDuration = 1.0f; - - private float launchImpulse; - private PrismaticJoint stickJoint; - private Body stickTarget; private readonly Attack attack; private Vector2 launchPos; + private readonly HashSet hits = new HashSet(); + public List IgnoredBodies; private Character user; @@ -70,14 +70,15 @@ namespace Barotrauma.Items.Components } } + public IEnumerable Hits + { + get { return hits; } + } + private float persistentStickJointTimer; [Serialize(10.0f, false, description: "The impulse applied to the physics body of the item when it's launched. Higher values make the projectile faster.")] - public float LaunchImpulse - { - get { return launchImpulse; } - set { launchImpulse = value; } - } + public float LaunchImpulse { get; set; } [Serialize(0.0f, false, description: "The rotation of the item relative to the rotation of the weapon when launched (in degrees).")] public float LaunchRotation @@ -100,6 +101,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, false, description: "When set to true, the item won't fall of a target it's stuck to unless removed.")] + public bool StickPermanently + { + get; + set; + } + [Serialize(false, false, description: "Can the item stick to the character it hits.")] public bool StickToCharacters { @@ -138,6 +146,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(1, false, description: "How many targets the projectile can hit before it stops.")] + public int MaxTargetsToHit + { + get; + set; + } + [Serialize(false, false, description: "Should the item be deleted when it hits something.")] public bool RemoveOnHit { @@ -152,6 +167,17 @@ namespace Barotrauma.Items.Components set; } + public Body StickTarget + { + get; + private set; + } + + public bool IsStuckToTarget + { + get { return StickTarget != null; } + } + public Projectile(Item item, XElement element) : base (item, element) { @@ -159,7 +185,7 @@ namespace Barotrauma.Items.Components foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "attack") continue; + if (!subElement.Name.ToString().Equals("attack", StringComparison.OrdinalIgnoreCase)) { continue; } attack = new Attack(subElement, item.Name + ", Projectile"); } } @@ -203,7 +229,7 @@ namespace Barotrauma.Items.Components } else { - Launch(launchDir * launchImpulse * item.body.Mass); + Launch(launchDir * LaunchImpulse * item.body.Mass); } } @@ -214,6 +240,9 @@ namespace Barotrauma.Items.Components private void Launch(Vector2 impulse) { + hits.Clear(); + MaxTargetsToHit = 2; + if (item.AiTarget != null) { item.AiTarget.SightRange = item.AiTarget.MaxSightRange; @@ -237,7 +266,7 @@ namespace Barotrauma.Items.Components if (stickJoint == null) { return; } - stickTarget = null; + StickTarget = null; GameMain.World.Remove(stickJoint); stickJoint = null; } @@ -265,6 +294,12 @@ namespace Barotrauma.Items.Components { //shooting indoors, do a hitscan outside as well hits.AddRange(DoRayCast(rayStart + item.Submarine.SimPosition, rayEnd + item.Submarine.SimPosition)); + //also in the coordinate space of docked subs + foreach (Submarine dockedSub in item.Submarine.DockedTo) + { + if (dockedSub == item.Submarine) { continue; } + hits.AddRange(DoRayCast(rayStart + item.Submarine.SimPosition - dockedSub.SimPosition, rayEnd + item.Submarine.SimPosition - dockedSub.SimPosition)); + } } else { @@ -291,7 +326,7 @@ namespace Barotrauma.Items.Components foreach (HitscanResult h in hits) { item.body.SetTransform(h.Point, rotation); - if (HandleProjectileCollision(h.Fixture, h.Normal)) + if (HandleProjectileCollision(h.Fixture, h.Normal, Vector2.Zero)) { hitSomething = true; break; @@ -325,7 +360,7 @@ namespace Barotrauma.Items.Components { //ignore sensors and items if (fixture?.Body == null || fixture.IsSensor) { return true; } - if (fixture.Body.UserData is Item) { return true; } + if (fixture.Body.UserData is Item item && (item.GetComponent() == null && !item.Prefab.DamagedByProjectiles || item.Condition <= 0)) { return true; } if (fixture.Body?.UserData as string == "ruinroom") { return true; } //ignore everything else than characters, sub walls and level walls @@ -345,7 +380,7 @@ namespace Barotrauma.Items.Components //ignore sensors and items if (fixture?.Body == null || fixture.IsSensor) { return -1; } - if (fixture.Body.UserData is Item item && item.GetComponent() == null) { return -1; } + if (fixture.Body.UserData is Item item && (item.GetComponent() == null && !item.Prefab.DamagedByProjectiles || item.Condition <= 0)) { return -1; } if (fixture.Body?.UserData as string == "ruinroom") { return -1; } //ignore everything else than characters, sub walls and level walls @@ -368,7 +403,7 @@ namespace Barotrauma.Items.Components while (impactQueue.Count > 0) { var impact = impactQueue.Dequeue(); - HandleProjectileCollision(impact.Fixture, impact.Normal); + HandleProjectileCollision(impact.Fixture, impact.Normal, impact.LinearVelocity); } if (item.body != null && item.body.FarseerBody.IsBullet) @@ -383,25 +418,31 @@ namespace Barotrauma.Items.Components if (stickJoint == null) { return; } - if (persistentStickJointTimer > 0.0f) + if (persistentStickJointTimer > 0.0f && !StickPermanently) { persistentStickJointTimer -= deltaTime; return; } - if (stickJoint.JointTranslation < stickJoint.LowerLimit * 0.9f || stickJoint.JointTranslation > stickJoint.UpperLimit * 0.9f) + if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { - stickTarget = null; - if (stickJoint != null) + if (StickTargetRemoved() || + (!StickPermanently && (stickJoint.JointTranslation < stickJoint.LowerLimit * 0.9f || stickJoint.JointTranslation > stickJoint.UpperLimit * 0.9f))) { - if (GameMain.World.JointList.Contains(stickJoint)) - { - GameMain.World.Remove(stickJoint); - } - stickJoint = null; + Unstick(); +#if SERVER + item.CreateServerEvent(this); +#endif } - if (!item.body.FarseerBody.IsBullet) { IsActive = false; } - } + } + } + + private bool StickTargetRemoved() + { + if (StickTarget == null) { return true; } + if (StickTarget.UserData is Limb limb) { return limb.character.Removed; } + if (StickTarget.UserData is Entity entity) { return entity.Removed; } + return false; } @@ -409,6 +450,12 @@ namespace Barotrauma.Items.Components { if (User != null && User.Removed) { User = null; return false; } if (IgnoredBodies.Contains(target.Body)) { return false; } + //ignore character colliders (the projectile only hits limbs) + if (target.CollisionCategories == Physics.CollisionCharacter && target.Body.UserData is Character) + { + return false; + } + if (hits.Contains(target.Body)) { return false; } if (target.Body.UserData is Submarine sub) { Vector2 dir = item.body.LinearVelocity.LengthSquared() < 0.001f ? @@ -424,6 +471,7 @@ namespace Barotrauma.Items.Components Vector2.Dot(item.body.SimPosition - launchPos, dir) > 0) { target = wallBody.FixtureList.First(); + if (hits.Contains(target.Body)) { return false; } } else { @@ -446,18 +494,23 @@ namespace Barotrauma.Items.Components return false; } - impactQueue.Enqueue(new Impact(target, contact.Manifold.LocalNormal)); - item.body.FarseerBody.OnCollision -= OnProjectileCollision; - - return true; + hits.Add(target.Body); + impactQueue.Enqueue(new Impact(target, contact.Manifold.LocalNormal, item.body.LinearVelocity)); + if (hits.Count() >= MaxTargetsToHit) + { + item.body.FarseerBody.OnCollision -= OnProjectileCollision; + return true; + } + else + { + return false; + } } - private bool HandleProjectileCollision(Fixture target, Vector2 collisionNormal) + private bool HandleProjectileCollision(Fixture target, Vector2 collisionNormal, Vector2 velocity) { if (User != null && User.Removed) { User = null; } - if (IgnoredBodies.Contains(target.Body)) { return false; } - //ignore character colliders (the projectile only hits limbs) if (target.CollisionCategories == Physics.CollisionCharacter && target.Body.UserData is Character) { @@ -478,7 +531,6 @@ namespace Barotrauma.Items.Components //severed limbs don't deactivate the projectile (but may still slow it down enough to make it inactive) if (limb.IsSevered) { - target.Body.ApplyLinearImpulse(item.body.LinearVelocity * item.body.Mass); return true; } @@ -488,7 +540,7 @@ namespace Barotrauma.Items.Components } else if (target.Body.UserData is Item targetItem) { - if (attack != null && targetItem.Prefab.DamagedByProjectiles) + if (attack != null && targetItem.Prefab.DamagedByProjectiles && targetItem.Condition > 0) { attackResult = attack.DoDamage(User, targetItem, item.WorldPosition, 1.0f); } @@ -558,21 +610,30 @@ namespace Barotrauma.Items.Components } } - item.body.FarseerBody.OnCollision -= OnProjectileCollision; + target.Body.ApplyLinearImpulse(velocity * item.body.Mass); - item.body.CollisionCategories = Physics.CollisionItem; - item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel; - - IgnoredBodies.Clear(); - - target.Body.ApplyLinearImpulse(item.body.LinearVelocity * item.body.Mass); + if (hits.Count() >= MaxTargetsToHit) + { + item.body.FarseerBody.OnCollision -= OnProjectileCollision; + if ((item.Prefab.DamagedByProjectiles || item.Prefab.DamagedByMeleeWeapons) && item.Condition > 0) + { + item.body.CollisionCategories = Physics.CollisionCharacter; + item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform | Physics.CollisionProjectile; + } + else + { + item.body.CollisionCategories = Physics.CollisionItem; + item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel; + } + IgnoredBodies.Clear(); + } if (attackResult.AppliedDamageModifiers != null && attackResult.AppliedDamageModifiers.Any(dm => dm.DeflectProjectiles)) { item.body.LinearVelocity *= 0.1f; } - else if (Vector2.Dot(item.body.LinearVelocity, collisionNormal) < 0.0f && + else if (Vector2.Dot(velocity, collisionNormal) < 0.0f && hits.Count() >= MaxTargetsToHit && (DoesStick || (StickToCharacters && target.Body.UserData is Limb) || (StickToStructures && target.Body.UserData is Structure) || @@ -581,8 +642,24 @@ namespace Barotrauma.Items.Components Vector2 dir = new Vector2( (float)Math.Cos(item.body.Rotation), (float)Math.Sin(item.body.Rotation)); - - StickToTarget(target.Body, dir); + + if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) + { + if (target.Body.UserData is Structure structure && structure.Submarine != item.Submarine && structure.Submarine != null) + { + StickToTarget(structure.Submarine.PhysicsBody.FarseerBody, dir); + } + else + { + StickToTarget(target.Body, dir); + } + } +#if SERVER + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) + { + item.CreateServerEvent(this); + } +#endif item.body.LinearVelocity *= 0.5f; return Hitscan; @@ -614,7 +691,7 @@ namespace Barotrauma.Items.Components private void StickToTarget(Body targetBody, Vector2 axis) { - if (stickJoint != null) return; + if (stickJoint != null) { return; } stickJoint = new PrismaticJoint(targetBody, item.body.FarseerBody, item.body.SimPosition, axis, true) { @@ -622,19 +699,39 @@ namespace Barotrauma.Items.Components MaxMotorForce = 30.0f, LimitEnabled = true }; - if (item.Sprite != null) + + if (StickPermanently) { - stickJoint.LowerLimit = ConvertUnits.ToSimUnits(item.Sprite.size.X * -0.3f); - stickJoint.UpperLimit = ConvertUnits.ToSimUnits(item.Sprite.size.X * 0.3f); + stickJoint.LowerLimit = stickJoint.UpperLimit = 0.0f; + item.body.ResetDynamics(); + } + else if (item.Sprite != null) + { + stickJoint.LowerLimit = ConvertUnits.ToSimUnits(item.Sprite.size.X * -0.3f * item.Scale); + stickJoint.UpperLimit = ConvertUnits.ToSimUnits(item.Sprite.size.X * 0.3f * item.Scale); } persistentStickJointTimer = PersistentStickJointDuration; - stickTarget = targetBody; + StickTarget = targetBody; GameMain.World.Add(stickJoint); IsActive = true; } + private void Unstick() + { + StickTarget = null; + if (stickJoint != null) + { + if (GameMain.World.JointList.Contains(stickJoint)) + { + GameMain.World.Remove(stickJoint); + } + stickJoint = null; + } + if (!item.body.FarseerBody.IsBullet) { IsActive = false; } + } + protected override void RemoveComponentSpecific() { if (stickJoint != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index 4544bd612..1b8e07500 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -1,4 +1,5 @@ -using Barotrauma.Networking; +using Barotrauma.Extensions; +using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Globalization; @@ -111,7 +112,7 @@ namespace Barotrauma.Items.Components element.GetAttributeString("name", ""); //backwards compatibility - var showRepairUIAttribute = element.Attributes().FirstOrDefault(a => a.Name.ToString().ToLowerInvariant() == "showrepairuithreshold"); + var showRepairUIAttribute = element.Attributes().FirstOrDefault(a => a.Name.ToString().Equals("showrepairuithreshold", StringComparison.OrdinalIgnoreCase)); if (showRepairUIAttribute != null) { float repairThreshold; @@ -130,7 +131,26 @@ namespace Barotrauma.Items.Components } partial void InitProjSpecific(XElement element); - + + /// + /// Check if the character manages to succesfully repair the item + /// + public bool CheckCharacterSuccess(Character character) + { + if (character == null) { return false; } + + if (statusEffectLists == null || statusEffectLists.None(s => s.Key == ActionType.OnFailure)) { return true; } + + // unpowered (electrical) items can be repaired without a risk of electrical shock + if (requiredSkills.Any(s => s != null && s.Identifier.Equals("electrical", StringComparison.OrdinalIgnoreCase)) && + item.GetComponent() is Powered powered && powered.Voltage < 0.1f) { return true; } + + if (Rand.Range(0.0f, 0.5f) < DegreeOfSuccess(character)) { return true; } + + ApplyStatusEffects(ActionType.OnFailure, 1.0f, character); + return false; + } + public bool StartRepairing(Character character, FixActions action) { if (character == null || character.IsDead || action == FixActions.None) @@ -143,8 +163,15 @@ namespace Barotrauma.Items.Components #if SERVER if (CurrentFixer != character || currentFixerAction != action) { + if (!CheckCharacterSuccess(character)) + { + GameMain.Server?.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnFailure, this, character.ID }); + return false; + } item.CreateServerEvent(this); } +#else + if (GameMain.Client == null && (CurrentFixer != character || currentFixerAction != action) && !CheckCharacterSuccess(character)) { return false; } #endif CurrentFixer = character; CurrentFixerAction = action; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs new file mode 100644 index 000000000..87db1cef9 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Rope.cs @@ -0,0 +1,256 @@ +using Barotrauma.Networking; +using FarseerPhysics; +using FarseerPhysics.Dynamics; +using Microsoft.Xna.Framework; +using System; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma.Items.Components +{ + partial class Rope : ItemComponent, IServerSerializable + { + private Item source, target; + + private float snapTimer; + private const float SnapAnimDuration = 1.0f; + + private float raycastTimer; + private const float RayCastInterval = 0.2f; + + [Serialize(0.0f, false, description: "How much force is applied to pull the projectile the rope is attached to.")] + public float ProjectilePullForce + { + get; + set; + } + + [Serialize(0.0f, false, description: "How much force is applied to pull the target the rope is attached to.")] + public float TargetPullForce + { + get; + set; + } + + [Serialize(0.0f, false, description: "How much force is applied to pull the source the rope is attached to.")] + public float SourcePullForce + { + get; + set; + } + + [Serialize(1000.0f, false, description: "How far the source item can be from the projectile until the rope breaks.")] + public float MaxLength + { + get; + set; + } + + [Serialize(true, false, description: "Should the rope snap when it collides with a structure/submarine (if not, it will just go through it).")] + public bool SnapOnCollision + { + get; + set; + } + + private bool snapped; + public bool Snapped + { + get { return snapped; } + set + { + if (snapped == value) { return; } + if (GameMain.NetworkMember != null) + { + if (GameMain.NetworkMember.IsClient) + { + return; + } + else + { +#if SERVER + item.CreateServerEvent(this); +#endif + } + } + snapped = value; + } + } + + public Rope(Item item, XElement element) : base(item, element) + { + InitProjSpecific(element); + } + + partial void InitProjSpecific(XElement element); + + + public void Attach(Item source, Item target) + { + System.Diagnostics.Debug.Assert(source != null); + System.Diagnostics.Debug.Assert(target != null); + this.source = source; + this.target = target; + ApplyStatusEffects(ActionType.OnUse, 1.0f, worldPosition: item.WorldPosition); + IsActive = true; + } + + public override void Update(float deltaTime, Camera cam) + { + if (source == null || source.Removed || target == null || target.Removed) + { + IsActive = false; + return; + } + + if (Snapped) + { + snapTimer += deltaTime; + if (snapTimer >= SnapAnimDuration) + { + IsActive = false; + } + return; + } + + Vector2 diff = target.WorldPosition - source.WorldPosition; + if (diff.LengthSquared() > MaxLength * MaxLength) + { + Snapped = true; + return; + } + +#if CLIENT + item.ResetCachedVisibleSize(); +#endif + + if (SnapOnCollision) + { + raycastTimer += deltaTime; + if (raycastTimer > RayCastInterval) + { + if (Submarine.PickBody(ConvertUnits.ToSimUnits(source.WorldPosition), ConvertUnits.ToSimUnits(target.WorldPosition), + collisionCategory: Physics.CollisionLevel | Physics.CollisionWall, + customPredicate: (Fixture f) => + { + var projectile = target?.GetComponent(); + if (projectile != null) + { + foreach (Body body in projectile.Hits) + { + Submarine alreadyHitSub = null; + if (body.UserData is Structure hitStructure) + { + alreadyHitSub = hitStructure.Submarine; + } + else if (body.UserData is Submarine hitSub) + { + alreadyHitSub = hitSub; + } + if (alreadyHitSub != null) + { + if (f.Body?.UserData is MapEntity me && me.Submarine == alreadyHitSub) { return false; } + if (f.Body?.UserData as Submarine == alreadyHitSub) { return false; } + } + } + } + Submarine targetSub = target?.GetComponent()?.StickTarget?.UserData as Submarine ?? target.Submarine; + + if (f.Body?.UserData is MapEntity mapEntity && mapEntity.Submarine != null) + { + if (mapEntity.Submarine == targetSub || mapEntity.Submarine == source.Submarine) + { + return false; + } + } + else if (f.Body?.UserData is Submarine sub) + { + if (sub == targetSub || sub == source.Submarine) + { + return false; + } + } + return true; + }) != null) + { + Snapped = true; + return; + } + raycastTimer = 0.0f; + } + } + + Vector2 forceDir = diff; + if (forceDir.LengthSquared() > 0.01f) + { + forceDir = Vector2.Normalize(forceDir); + } + + if (Math.Abs(ProjectilePullForce) > 0.001f) + { + var projectile = target.GetComponent(); + projectile?.Item?.body?.ApplyForce(-forceDir * ProjectilePullForce); + } + + if (Math.Abs(SourcePullForce) > 0.001f) + { + var sourceBody = GetBodyToPull(source); + if (sourceBody != null) + { + sourceBody.ApplyForce(forceDir * SourcePullForce); + } + } + + if (Math.Abs(TargetPullForce) > 0.001f) + { + var targetBody = GetBodyToPull(target); + if (targetBody != null) + { + targetBody.ApplyForce(-forceDir * TargetPullForce); + } + } + } + + public override void UpdateBroken(float deltaTime, Camera cam) + { + base.UpdateBroken(deltaTime, cam); + if (Snapped) + { + snapTimer += deltaTime; + if (snapTimer >= SnapAnimDuration) + { + IsActive = false; + } + } + } + + private PhysicsBody GetBodyToPull(Item target) + { + if (target.ParentInventory is CharacterInventory characterInventory && + characterInventory.Owner is Character ownerCharacter) + { + if (ownerCharacter.Removed) { return null; } + return ownerCharacter.AnimController.Collider; + } + var projectile = target.GetComponent(); + if (projectile != null) + { + if (projectile.StickTarget?.UserData is Structure structure) + { + return structure.Submarine?.PhysicsBody; + } + else if (projectile.StickTarget?.UserData is Submarine sub) + { + return sub?.PhysicsBody; + } + else if (projectile.StickTarget?.UserData is Character character) + { + return character.AnimController.Collider; + } + return null; + } + if (target.body != null) { return target.body; } + return null; + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs index ada7e145b..edbfddcdd 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Connection.cs @@ -88,7 +88,7 @@ namespace Barotrauma.Items.Components { foreach (XElement subElement in item.Prefab.ConfigElement.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "connectionpanel") { continue; } + if (!subElement.Name.ToString().Equals("connectionpanel", StringComparison.OrdinalIgnoreCase)) { continue; } foreach (XElement connectionElement in subElement.Elements()) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs index a58dc17e3..6d48bb623 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/ConnectionPanel.cs @@ -192,6 +192,8 @@ namespace Barotrauma.Items.Components public bool CheckCharacterSuccess(Character character) { if (character == null) { return false; } + //no electrocution in sub editor + if (Screen.Selected == GameMain.SubEditorScreen) { return true; } var powered = item.GetComponent(); if (powered != null) @@ -203,7 +205,7 @@ namespace Barotrauma.Items.Components float degreeOfSuccess = DegreeOfSuccess(character); if (Rand.Range(0.0f, 0.5f) < degreeOfSuccess) { return true; } - item.ApplyStatusEffects(ActionType.OnFailure, 1.0f, character); + ApplyStatusEffects(ActionType.OnFailure, 1.0f, character); return false; } @@ -262,7 +264,18 @@ namespace Barotrauma.Items.Components { if (wire.OtherConnection(null) == null) //wire not connected to anything else { +#if CLIENT + if (SubEditorScreen.IsSubEditor()) + { + wire.Item.Remove(); + } + else + { + wire.Item.Drop(null); + } +#else wire.Item.Drop(null); +#endif } } @@ -275,7 +288,18 @@ namespace Barotrauma.Items.Components if (wire.OtherConnection(c) == null) //wire not connected to anything else { +#if CLIENT + if (SubEditorScreen.IsSubEditor()) + { + wire.Item.Remove(); + } + else + { + wire.Item.Drop(null); + } +#else wire.Item.Drop(null); +#endif } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs index 6f0ca45b8..66ce55965 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/CustomInterface.cs @@ -12,6 +12,7 @@ namespace Barotrauma.Items.Components public bool ContinuousSignal; public bool State; public string ConnectionName; + public string PropertyName; public Connection Connection; [Serialize("", false, translationTextTag: "Label.", description: "The text displayed on this button/tickbox."), Editable] public string Label { get; set; } @@ -28,11 +29,12 @@ namespace Barotrauma.Items.Components { Label = element.GetAttributeString("text", ""); ConnectionName = element.GetAttributeString("connection", ""); + PropertyName = element.GetAttributeString("propertyname", "").ToLowerInvariant(); Signal = element.GetAttributeString("signal", "1"); foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() == "statuseffect") + if (subElement.Name.ToString().Equals("statuseffect", System.StringComparison.OrdinalIgnoreCase)) { StatusEffects.Add(StatusEffect.Load(subElement, parentDebugName: "custom interface element (label " + Label + ")")); } @@ -89,6 +91,7 @@ namespace Barotrauma.Items.Components switch (subElement.Name.ToString().ToLowerInvariant()) { case "button": + case "textbox": var button = new CustomInterfaceElement(subElement) { ContinuousSignal = false @@ -106,7 +109,7 @@ namespace Barotrauma.Items.Components }; if (string.IsNullOrEmpty(tickBox.Label)) { - tickBox.Label = "Signal out " + customInterfaceElementList.Count(e => !e.ContinuousSignal); + tickBox.Label = "Signal out " + customInterfaceElementList.Count(e => e.ContinuousSignal); } customInterfaceElementList.Add(tickBox); break; @@ -168,6 +171,18 @@ namespace Barotrauma.Items.Components tickBoxElement.State = state; } + private void TextChanged(CustomInterfaceElement textElement, string text) + { + textElement.Signal = text; + foreach (ISerializableEntity e in item.AllPropertyObjects) + { + if (e.SerializableProperties.ContainsKey(textElement.PropertyName)) + { + e.SerializableProperties[textElement.PropertyName].TrySetValue(e, text); + } + } + } + public override void Update(float deltaTime, Camera cam) { UpdateProjSpecific(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs index 67c7e815b..9aba89bd0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs @@ -15,13 +15,13 @@ namespace Barotrauma.Items.Components private float lightBrightness; private float blinkFrequency; private float range; - private float flicker; + private float flicker, flickerState; private bool castShadows; private bool drawBehindSubs; private float blinkTimer; - private bool itemLoaded; + private double lastToggleSignalTime; public PhysicsBody ParentBody; @@ -34,6 +34,7 @@ namespace Barotrauma.Items.Components { range = MathHelper.Clamp(value, 0.0f, 4096.0f); #if CLIENT + item.ResetCachedVisibleSize(); if (light != null) { light.Range = range; } #endif } @@ -78,13 +79,11 @@ namespace Barotrauma.Items.Components if (IsActive == value) { return; } IsActive = value; -#if SERVER - if (GameMain.Server != null && itemLoaded) { item.CreateServerEvent(this); } -#endif + OnStateChanged(); } } - [Serialize(0.0f, false, description: "How heavily the light flickers. 0 = no flickering, 1 = the light will alternate between completely dark and full brightness.")] + [Editable, Serialize(0.0f, false, description: "How heavily the light flickers. 0 = no flickering, 1 = the light will alternate between completely dark and full brightness.")] public float Flicker { get { return flicker; } @@ -94,6 +93,13 @@ namespace Barotrauma.Items.Components } } + [Editable, Serialize(1.0f, false, description: "How fast the light flickers.")] + public float FlickerSpeed + { + get; + set; + } + [Editable, Serialize(0.0f, true, description: "How rapidly the light blinks on and off (in Hz). 0 = no blinking.")] public float BlinkFrequency { @@ -117,6 +123,13 @@ namespace Barotrauma.Items.Components } } + [Serialize(false, false, description: "If enabled, the component will ignore continuous signals received in the toggle input (i.e. a continuous signal will only toggle it once).")] + public bool IgnoreContinuousToggle + { + get; + set; + } + public override void Move(Vector2 amount) { #if CLIENT @@ -158,14 +171,7 @@ namespace Barotrauma.Items.Components IsActive = IsOn; item.AddTag("light"); } - - public override void OnItemLoaded() - { - base.OnItemLoaded(); - itemLoaded = true; - SetLightSourceState(IsActive, lightBrightness); - } - + public override void Update(float deltaTime, Camera cam) { if (item.AiTarget != null) @@ -219,7 +225,7 @@ namespace Barotrauma.Items.Components } else { - lightBrightness = MathHelper.Lerp(lightBrightness, Math.Min(Voltage, 1.0f), 0.1f); + lightBrightness = MathHelper.Lerp(lightBrightness, powerConsumption <= 0.0f ? 1.0f : Math.Min(Voltage, 1.0f), 0.1f); } if (blinkFrequency > 0.0f) @@ -233,7 +239,10 @@ namespace Barotrauma.Items.Components } else { - SetLightSourceState(true, lightBrightness * (1.0f - Rand.Range(0.0f, flicker))); + flickerState += deltaTime * FlickerSpeed; + flickerState %= 255; + float noise = PerlinNoise.GetPerlin(flickerState, flickerState * 0.5f) * flicker; + SetLightSourceState(true, lightBrightness * (1.0f - noise)); } if (powerIn == null && powerConsumption > 0.0f) { Voltage -= deltaTime; } @@ -249,15 +258,21 @@ namespace Barotrauma.Items.Components return true; } + partial void OnStateChanged(); + public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f) { switch (connection.Name) { case "toggle": - IsActive = !IsActive; + if (!IgnoreContinuousToggle || lastToggleSignalTime < Timing.TotalTime - 0.1) + { + IsOn = !IsOn; + } + lastToggleSignalTime = Timing.TotalTime; break; case "set_state": - IsActive = (signal != "0"); + IsOn = signal != "0"; break; case "set_color": LightColor = XMLExtensions.ParseColor(signal, false); @@ -265,11 +280,6 @@ namespace Barotrauma.Items.Components } } - public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) - { - msg.Write(IsOn); - } - private void UpdateAITarget(AITarget target) { if (!IsActive) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs index a672b8e63..9126c5884 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/MotionSensor.cs @@ -40,6 +40,9 @@ namespace Barotrauma.Items.Components set { rangeX = MathHelper.Clamp(value, 0.0f, 1000.0f); +#if CLIENT + item.ResetCachedVisibleSize(); +#endif } } [InGameEditable, Serialize(0.0f, true, description: "Vertical movement detection range.")] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs index a1da8e1d1..e5f76281f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/WifiComponent.cs @@ -26,7 +26,13 @@ namespace Barotrauma.Items.Components public float Range { get { return range; } - set { range = Math.Max(value, 0.0f); } + set + { + range = Math.Max(value, 0.0f); +#if CLIENT + item.ResetCachedVisibleSize(); +#endif + } } [InGameEditable, Serialize(1, true, description: "WiFi components can only communicate with components that use the same channel.")] @@ -39,6 +45,14 @@ namespace Barotrauma.Items.Components } } + + [Serialize(false, false, description: "Can the component communicate with wifi components in another team's submarine (e.g. enemy sub in Combat missions, respawn shuttle). Needs to be enabled on both the component transmitting the signal and the component receiving it.")] + public bool AllowCrossTeamCommunication + { + get; + set; + } + [Editable, Serialize(false, false, description: "If enabled, any signals received from another chat-linked wifi component are displayed " + "as chat messages in the chatbox of the player holding the item.")] public bool LinkToChat @@ -84,7 +98,7 @@ namespace Barotrauma.Items.Components { if (sender == null || sender.channel != channel) { return false; } - if (sender.TeamID != TeamID) + if (sender.TeamID != TeamID && !AllowCrossTeamCommunication) { return false; } @@ -97,6 +111,10 @@ namespace Barotrauma.Items.Components public override void Update(float deltaTime, Camera cam) { chatMsgCooldown -= deltaTime; + if (chatMsgCooldown <= 0.0f) + { + IsActive = false; + } } public void TransmitSignal(int stepsTaken, string signal, Item source, Character sender, bool sendToChat, float signalStrength = 1.0f) @@ -164,7 +182,11 @@ namespace Barotrauma.Items.Components } } } - if (chatMsgSent) chatMsgCooldown = MinChatMessageInterval; + if (chatMsgSent) + { + chatMsgCooldown = MinChatMessageInterval; + IsActive = true; + } prevSignal = signal; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs index 94663b33e..d41d80358 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/Wire.cs @@ -5,6 +5,9 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Xml.Linq; +#if CLIENT +using Microsoft.Xna.Framework.Input; +#endif namespace Barotrauma.Items.Components { @@ -39,7 +42,7 @@ namespace Barotrauma.Items.Components const float MaxAttachDistance = 150.0f; - const float MinNodeDistance = 15.0f; + const float MinNodeDistance = 7.0f; const int MaxNodeCount = 255; const int MaxNodesPerNetworkEvent = 30; @@ -177,6 +180,8 @@ namespace Barotrauma.Items.Components newConnection.Item.Position : newConnection.Item.Position - refSub.HiddenSubPosition; + nodePos = RoundNode(nodePos); + if (nodes.Count > 0 && nodes[0] == nodePos) { break; } if (nodes.Count > 1 && nodes[nodes.Count - 1] == nodePos) { break; } @@ -334,7 +339,12 @@ namespace Barotrauma.Items.Components } else { +#if CLIENT + bool disableGrid = SubEditorScreen.IsSubEditor() && (PlayerInput.KeyDown(Keys.LeftShift) || PlayerInput.KeyDown(Keys.RightShift)); + newNodePos = disableGrid ? item.Position : RoundNode(item.Position); +#else newNodePos = RoundNode(item.Position); +#endif if (sub != null) { newNodePos -= sub.HiddenSubPosition; } canPlaceNode = true; } @@ -497,6 +507,9 @@ namespace Barotrauma.Items.Components sectionExtents.Y = Math.Max(Math.Abs(nodes[i].Y - item.Position.Y), sectionExtents.Y); } } +#if CLIENT + item.ResetCachedVisibleSize(); +#endif } public void ClearConnections(Character user = null) @@ -507,7 +520,7 @@ namespace Barotrauma.Items.Components foreach (Item item in Item.ItemList) { var connectionPanel = item.GetComponent(); - if (connectionPanel != null && connectionPanel.DisconnectedWires.Contains(this)) + if (connectionPanel != null && connectionPanel.DisconnectedWires.Contains(this) && !item.Removed) { #if SERVER item.CreateServerEvent(connectionPanel); @@ -526,18 +539,18 @@ namespace Barotrauma.Items.Components if (connections[0] != null && connections[1] != null) { - GameServer.Log(user.LogName + " disconnected a wire from " + + GameServer.Log(GameServer.CharacterLogName(user) + " disconnected a wire from " + connections[0].Item.Name + " (" + connections[0].Name + ") to "+ connections[1].Item.Name + " (" + connections[1].Name + ")", ServerLog.MessageType.ItemInteraction); } else if (connections[0] != null) { - GameServer.Log(user.LogName + " disconnected a wire from " + + GameServer.Log(GameServer.CharacterLogName(user) + " disconnected a wire from " + connections[0].Item.Name + " (" + connections[0].Name + ")", ServerLog.MessageType.ItemInteraction); } else if (connections[1] != null) { - GameServer.Log(user.LogName + " disconnected a wire from " + + GameServer.Log(GameServer.CharacterLogName(user) + " disconnected a wire from " + connections[1].Item.Name + " (" + connections[1].Name + ")", ServerLog.MessageType.ItemInteraction); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs index 49370a3a4..df66ea5c5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs @@ -34,7 +34,15 @@ namespace Barotrauma.Items.Components private int failedLaunchAttempts; + private readonly List activeProjectiles = new List(); + public IEnumerable ActiveProjectiles => activeProjectiles; + private Character user; + + public float Rotation + { + get { return rotation; } + } [Serialize("0,0", false, description: "The position of the barrel relative to the upper left corner of the base sprite (in pixels).")] public Vector2 BarrelPos @@ -72,6 +80,41 @@ namespace Barotrauma.Items.Components set { reloadTime = value; } } + [Editable(0.1f, 10f), Serialize(1.0f, false, description: "Modifies the duration of retraction of the barrell after recoil to get back to the original position after shooting. Reload time affects this too.")] + public float RetractionDurationMultiplier + { + get; + set; + } + + [Editable(0.1f, 10f), Serialize(0.1f, false, description: "How quickly the recoil moves the barrel after launching.")] + public float RecoilTime + { + get; + set; + } + + [Editable(0f, 1000f), Serialize(0f, false, description: "How long the barrell stays in place after the recoil and before retracting back to the original position.")] + public float RetractionDelay + { + get; + set; + } + + [Serialize(1, false, description: "How projectiles the weapon launches when fired once.")] + public int ProjectileCount + { + get; + set; + } + + [Serialize(false, false, description: "Can the turret be fired without projectiles (causing it just to execute the OnUse effects and the firing animation without actually firing anything).")] + public bool LaunchWithoutProjectile + { + get; + set; + } + [Editable, Serialize("0.0,0.0", true, description: "The range at which the barrel can rotate. TODO")] public Vector2 RotationLimits { @@ -95,6 +138,13 @@ namespace Barotrauma.Items.Components } } + [Serialize(0.0f, false, description: "Random spread applied to the firing angle of the projectiles (in degrees).")] + public float Spread + { + get; + set; + } + [Editable(0.0f, 1000.0f, DecimalCount = 2), Serialize(5.0f, false, description: "How much torque is applied to rotate the barrel when the item is used by a character" + " with insufficient skills to operate it. Higher values make the barrel rotate faster.")] @@ -155,7 +205,21 @@ namespace Barotrauma.Items.Components UpdateTransformedBarrelPos(); } } - + + [Serialize(3000.0f, true, description: "How close to a target the turret has to be for an AI character to fire it.")] + public float AIRange + { + get; + set; + } + + [Serialize(-1, true, description: "The turret won't fire additional projectiles if the number of previously fired, still active projectiles reaches this limit. If set to -1, there is no limit to the number of projectiles.")] + public int MaxActiveProjectiles + { + get; + set; + } + public Turret(Item item, XElement element) : base(item, element) { @@ -187,6 +251,7 @@ namespace Barotrauma.Items.Components //if (item.FlippedY) flippedRotation = 180.0f - flippedRotation; transformedBarrelPos = MathUtils.RotatePointAroundTarget(barrelPos * item.Scale, new Vector2(item.Rect.Width / 2, item.Rect.Height / 2), flippedRotation); #if CLIENT + item.ResetCachedVisibleSize(); item.SpriteRotation = MathHelper.ToRadians(flippedRotation); #endif } @@ -213,13 +278,18 @@ namespace Barotrauma.Items.Components { this.cam = cam; - if (reload > 0.0f) reload -= deltaTime; + if (reload > 0.0f) { reload -= deltaTime; } + + if (user != null && user.Removed) + { + user = null; + } ApplyStatusEffects(ActionType.OnActive, deltaTime, null); UpdateProjSpecific(deltaTime); - if (minRotation == maxRotation) return; + if (minRotation == maxRotation) { return; } float targetMidDiff = MathHelper.WrapAngle(targetRotation - (minRotation + maxRotation) / 2.0f); @@ -230,12 +300,19 @@ namespace Barotrauma.Items.Components targetRotation = (targetMidDiff < 0.0f) ? minRotation : maxRotation; } - float degreeOfSuccess = user == null ? 0.5f : DegreeOfSuccess(user); - if (degreeOfSuccess < 0.5f) degreeOfSuccess *= degreeOfSuccess; //the ease of aiming drops quickly with insufficient skill levels + float degreeOfSuccess = user == null ? 0.5f : DegreeOfSuccess(user); + if (degreeOfSuccess < 0.5f) { degreeOfSuccess *= degreeOfSuccess; } //the ease of aiming drops quickly with insufficient skill levels float springStiffness = MathHelper.Lerp(SpringStiffnessLowSkill, SpringStiffnessHighSkill, degreeOfSuccess); float springDamping = MathHelper.Lerp(SpringDampingLowSkill, SpringDampingHighSkill, degreeOfSuccess); float rotationSpeed = MathHelper.Lerp(RotationSpeedLowSkill, RotationSpeedHighSkill, degreeOfSuccess); + if (user?.Info != null) + { + user.Info.IncreaseSkillLevel("weapons", + SkillSettings.Current.SkillIncreasePerSecondWhenOperatingTurret * deltaTime / Math.Max(user.GetSkillLevel("weapons"), 1.0f), + user.WorldPosition + Vector2.UnitY * 150.0f); + } + angularVelocity += (MathHelper.WrapAngle(targetRotation - rotation) * springStiffness - angularVelocity * springDamping) * deltaTime; angularVelocity = MathHelper.Clamp(angularVelocity, -rotationSpeed, rotationSpeed); @@ -265,95 +342,110 @@ namespace Barotrauma.Items.Components public override bool Use(float deltaTime, Character character = null) { - if (!characterUsable && character != null) return false; + if (!characterUsable && character != null) { return false; } return TryLaunch(deltaTime, character); } - private bool TryLaunch(float deltaTime, Character character = null) + private bool TryLaunch(float deltaTime, Character character = null, bool ignorePower = false) { -#if CLIENT - if (GameMain.Client != null) return false; -#endif + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; } - if (reload > 0.0f) return false; + if (reload > 0.0f) { return false; } - if (GetAvailableBatteryPower() < powerConsumption) + if (MaxActiveProjectiles >= 0) { -#if CLIENT - if (!flashLowPower && character != null && character == Character.Controlled) + activeProjectiles.RemoveAll(it => it.Removed); + if (activeProjectiles.Count >= MaxActiveProjectiles) { - flashLowPower = true; - GUI.PlayUISound(GUISoundType.PickItemFail); + return false; } -#endif - return false; } - foreach (MapEntity e in item.linkedTo) + if (!ignorePower) { - //use linked projectile containers in case they have to react to the turret being launched somehow - //(play a sound, spawn more projectiles) - if (!(e is Item linkedItem)) continue; - ItemContainer projectileContainer = linkedItem.GetComponent(); - if (projectileContainer != null) + if (GetAvailableBatteryPower() < powerConsumption) { - linkedItem.Use(deltaTime, null); - var repairable = linkedItem.GetComponent(); - if (repairable != null) - { - repairable.LastActiveTime = (float)Timing.TotalTime + 1.0f; - } - } - } - - var projectiles = GetLoadedProjectiles(true); - if (projectiles.Count == 0) - { - //coilguns spawns ammo in the ammo boxes with the OnUse statuseffect when the turret is launched, - //causing a one frame delay before the gun can be launched (or more in multiplayer where there may be a longer delay) - // -> attempt to launch the gun multiple times before showing the "no ammo" flash - failedLaunchAttempts++; #if CLIENT - if (!flashNoAmmo && character != null && character == Character.Controlled && failedLaunchAttempts > 20) - { - flashNoAmmo = true; - failedLaunchAttempts = 0; - GUI.PlayUISound(GUISoundType.PickItemFail); - } -#endif - return false; - } - - failedLaunchAttempts = 0; - - var batteries = item.GetConnectedComponents(); - float neededPower = powerConsumption; - - while (neededPower > 0.0001f && batteries.Count > 0) - { - batteries.RemoveAll(b => b.Charge <= 0.0001f || b.MaxOutPut <= 0.0001f); - float takePower = neededPower / batteries.Count; - takePower = Math.Min(takePower, batteries.Min(b => Math.Min(b.Charge * 3600.0f, b.MaxOutPut))); - foreach (PowerContainer battery in batteries) - { - neededPower -= takePower; - battery.Charge -= takePower / 3600.0f; -#if SERVER - if (GameMain.Server != null) + if (!flashLowPower && character != null && character == Character.Controlled) { - battery.Item.CreateServerEvent(battery); + flashLowPower = true; + GUI.PlayUISound(GUISoundType.PickItemFail); } #endif + return false; } } - Launch(projectiles[0].Item, character); + Projectile launchedProjectile = null; + for (int i = 0; i < ProjectileCount; i++) + { + foreach (MapEntity e in item.linkedTo) + { + //use linked projectile containers in case they have to react to the turret being launched somehow + //(play a sound, spawn more projectiles) + if (!(e is Item linkedItem)) { continue; } + ItemContainer projectileContainer = linkedItem.GetComponent(); + if (projectileContainer != null) + { + linkedItem.Use(deltaTime, null); + var repairable = linkedItem.GetComponent(); + if (repairable != null && failedLaunchAttempts < 2) + { + repairable.LastActiveTime = (float)Timing.TotalTime + 1.0f; + } + } + } + var projectiles = GetLoadedProjectiles(true); + if (projectiles.Count == 0 && !LaunchWithoutProjectile) + { + //coilguns spawns ammo in the ammo boxes with the OnUse statuseffect when the turret is launched, + //causing a one frame delay before the gun can be launched (or more in multiplayer where there may be a longer delay) + // -> attempt to launch the gun multiple times before showing the "no ammo" flash + failedLaunchAttempts++; +#if CLIENT + if (!flashNoAmmo && character != null && character == Character.Controlled && failedLaunchAttempts > 20) + { + flashNoAmmo = true; + failedLaunchAttempts = 0; + GUI.PlayUISound(GUISoundType.PickItemFail); + } +#endif + return false; + } + failedLaunchAttempts = 0; + launchedProjectile = projectiles.FirstOrDefault(); + + if (!ignorePower) + { + var batteries = item.GetConnectedComponents(); + float neededPower = powerConsumption; + while (neededPower > 0.0001f && batteries.Count > 0) + { + batteries.RemoveAll(b => b.Charge <= 0.0001f || b.MaxOutPut <= 0.0001f); + float takePower = neededPower / batteries.Count; + takePower = Math.Min(takePower, batteries.Min(b => Math.Min(b.Charge * 3600.0f, b.MaxOutPut))); + foreach (PowerContainer battery in batteries) + { + neededPower -= takePower; + battery.Charge -= takePower / 3600.0f; +#if SERVER + battery.Item.CreateServerEvent(battery); +#endif + } + } + } + + if (launchedProjectile != null || LaunchWithoutProjectile) + { + Launch(launchedProjectile?.Item, character); + } + } #if SERVER - if (character != null) + if (character != null && launchedProjectile != null) { - string msg = character.LogName + " launched " + item.Name + " (projectile: " + projectiles[0].Item.Name; - var containedItems = projectiles[0].Item.ContainedItems; + string msg = GameServer.CharacterLogName(character) + " launched " + item.Name + " (projectile: " + launchedProjectile.Item.Name; + var containedItems = launchedProjectile.Item.ContainedItems; if (containedItems == null || !containedItems.Any()) { msg += ")"; @@ -369,29 +461,38 @@ namespace Barotrauma.Items.Components return true; } - private void Launch(Item projectile, Character user = null) + private void Launch(Item projectile, Character user = null, float? launchRotation = null) { reload = reloadTime; - projectile.Drop(null); - if (projectile.body == null) { return; } - - projectile.body.Dir = 1.0f; - projectile.body.ResetDynamics(); - projectile.body.Enabled = true; - projectile.SetTransform(ConvertUnits.ToSimUnits(new Vector2(item.WorldRect.X + transformedBarrelPos.X, item.WorldRect.Y - transformedBarrelPos.Y)), -rotation); - projectile.UpdateTransform(); - projectile.Submarine = projectile.body.Submarine; - - Projectile projectileComponent = projectile.GetComponent(); - if (projectileComponent != null) + if (projectile != null) { - projectileComponent.Use((float)Timing.Step); - projectileComponent.User = user; - } + activeProjectiles.Add(projectile); + projectile.Drop(null); + if (projectile.body != null) + { + projectile.body.Dir = 1.0f; + projectile.body.ResetDynamics(); + projectile.body.Enabled = true; + } - if (projectile.Container != null) projectile.Container.RemoveContained(projectile); - + float spread = MathHelper.ToRadians(Spread) * Rand.Range(-0.5f, 0.5f); + projectile.SetTransform( + ConvertUnits.ToSimUnits(new Vector2(item.WorldRect.X + transformedBarrelPos.X, item.WorldRect.Y - transformedBarrelPos.Y)), + -(launchRotation ?? rotation) + spread); + projectile.UpdateTransform(); + projectile.Submarine = projectile.body?.Submarine; + + Projectile projectileComponent = projectile.GetComponent(); + if (projectileComponent != null) + { + projectileComponent.Use((float)Timing.Step); + projectile.GetComponent()?.Attach(item, projectile); + projectileComponent.User = user; + } + + if (projectile.Container != null) { projectile.Container.RemoveContained(projectile); } + } if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer) { GameMain.NetworkMember.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ComponentState, item.GetComponentIndex(this), projectile }); @@ -403,6 +504,196 @@ namespace Barotrauma.Items.Components partial void LaunchProjSpecific(); + private float waitTimer; + private float disorderTimer; + + private float prevTargetRotation; + private float updateTimer; + private bool updatePending; + public void ThalamusOperate(WreckAI ai, float deltaTime, bool targetHumans, bool targetOtherCreatures, bool targetSubmarines, bool ignoreDelay) + { + if (ai == null) { return; } + + IsActive = true; + + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) + { + return; + } + + if (updatePending) + { + if (updateTimer < 0.0f) + { +#if SERVER + item.CreateServerEvent(this); +#endif + prevTargetRotation = targetRotation; + updateTimer = 0.25f; + } + updateTimer -= deltaTime; + } + + if (!ignoreDelay && waitTimer > 0) + { + waitTimer -= deltaTime; + return; + } + Submarine closestSub = null; + float maxDistance = 10000.0f; + float shootDistance = AIRange; + ISpatialEntity target = null; + float closestDist = shootDistance * shootDistance; + if (targetHumans || targetOtherCreatures) + { + foreach (var character in Character.CharacterList) + { + if (character == null || character.Removed || character.IsDead) { continue; } + if (character.Params.Group.Equals(ai.Config.Entity, StringComparison.OrdinalIgnoreCase)) { continue; } + bool isHuman = character.IsHuman || character.Params.Group.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase); + if (isHuman) + { + if (!targetHumans) + { + // Don't target humans if not defined to. + continue; + } + } + else if (!targetOtherCreatures) + { + // Don't target other creatures if not defined to. + continue; + } + float dist = Vector2.DistanceSquared(character.WorldPosition, item.WorldPosition); + if (dist > closestDist) { continue; } + target = character; + closestDist = dist; + } + } + if (targetSubmarines) + { + if (target == null || target.Submarine != null) + { + closestDist = maxDistance * maxDistance; + foreach (Submarine sub in Submarine.Loaded) + { + if (sub.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; } + float dist = Vector2.DistanceSquared(sub.WorldPosition, item.WorldPosition); + if (dist > closestDist) { continue; } + closestSub = sub; + closestDist = dist; + } + closestDist = shootDistance * shootDistance; + if (closestSub != null) + { + foreach (var hull in Hull.hullList) + { + if (!closestSub.IsEntityFoundOnThisSub(hull, true)) { continue; } + float dist = Vector2.DistanceSquared(hull.WorldPosition, item.WorldPosition); + if (dist > closestDist) { continue; } + target = hull; + closestDist = dist; + } + } + } + } + if (!ignoreDelay) + { + if (target == null) + { + // Random movement + waitTimer = Rand.Value(Rand.RandSync.Unsynced) < 0.98f ? 0f : Rand.Range(5f, 20f); + targetRotation = Rand.Range(minRotation, maxRotation); + updatePending = true; + return; + } + if (disorderTimer < 0) + { + // Random disorder + disorderTimer = Rand.Range(0f, 3f); + waitTimer = Rand.Range(0.25f, 1f); + targetRotation = MathUtils.WrapAngleTwoPi(targetRotation += Rand.Range(-1f, 1f)); + updatePending = true; + return; + } + else + { + disorderTimer -= deltaTime; + } + } + if (target == null) { return; } + + float angle = -MathUtils.VectorToAngle(target.WorldPosition - item.WorldPosition); + targetRotation = MathUtils.WrapAngleTwoPi(angle); + + if (Math.Abs(targetRotation - prevTargetRotation) > 0.1f) { updatePending = true; } + + if (target is Hull targetHull) + { + Vector2 barrelDir = new Vector2((float)Math.Cos(rotation), -(float)Math.Sin(rotation)); + if (!MathUtils.GetLineRectangleIntersection(item.WorldPosition, item.WorldPosition + barrelDir * AIRange, targetHull.WorldRect, out _)) + { + return; + } + } + else + { + float midRotation = (minRotation + maxRotation) / 2.0f; + while (midRotation - angle < -MathHelper.Pi) { angle -= MathHelper.TwoPi; } + while (midRotation - angle > MathHelper.Pi) { angle += MathHelper.TwoPi; } + if (angle < minRotation || angle > maxRotation) { return; } + float enemyAngle = MathUtils.VectorToAngle(target.WorldPosition - item.WorldPosition); + float turretAngle = -rotation; + if (Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) > 0.15f) { return; } + } + + Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition); + Vector2 end = ConvertUnits.ToSimUnits(target.WorldPosition); + if (target.Submarine != null) + { + start -= target.Submarine.SimPosition; + end -= target.Submarine.SimPosition; + } + var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel; + var pickedBody = Submarine.PickBody(start, end, null, collisionCategories, allowInsideFixture: true); + if (pickedBody == null) { return; } + Character targetCharacter = null; + if (pickedBody.UserData is Character c) + { + targetCharacter = c; + } + else if (pickedBody.UserData is Limb limb) + { + targetCharacter = limb.character; + } + if (targetCharacter != null) + { + if (targetCharacter.Params.Group.Equals(ai.Config.Entity, StringComparison.OrdinalIgnoreCase)) + { + // Don't shoot friendly characters + return; + } + } + else + { + if (pickedBody.UserData is ISpatialEntity e) + { + Submarine sub = e.Submarine; + if (sub == null) { return; } + if (!targetSubmarines) { return; } + if (sub == Item.Submarine) { return; } + // Don't shoot non-player submarines, i.e. wrecks or outposts. + if (!sub.Info.IsPlayer) { return; } + } + else + { + // Hit something else, probably a level wall + return; + } + } + TryLaunch(deltaTime, ignorePower: true); + } + public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective) { if (character.AIController.SelectedAiTarget?.Entity is Character previousTarget && @@ -484,7 +775,7 @@ namespace Barotrauma.Items.Components //enough shells and power Character closestEnemy = null; - float closestDist = 3000 * 3000; + float closestDist = AIRange * AIRange; foreach (Character enemy in Character.CharacterList) { // Ignore dead, friendly, and those that are inside the same sub @@ -526,7 +817,7 @@ namespace Barotrauma.Items.Components end -= closestEnemy.Submarine.SimPosition; } var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel; - var pickedBody = Submarine.PickBody(start, end, null, collisionCategories); + var pickedBody = Submarine.PickBody(start, end, null, collisionCategories, allowInsideFixture: true); if (pickedBody == null) { return false; } Character targetCharacter = null; if (pickedBody.UserData is Character c) @@ -537,39 +828,40 @@ namespace Barotrauma.Items.Components { targetCharacter = limb.character; } - if (targetCharacter != null && HumanAIController.IsFriendly(character, targetCharacter)) + if (targetCharacter != null) { - // Don't shoot friendly characters - return false; + if (HumanAIController.IsFriendly(character, targetCharacter)) + { + // Don't shoot friendly characters + return false; + } } - else if (targetCharacter == null && !(pickedBody.UserData is Structure) && !(pickedBody.UserData is Item)) + else { - // Hit something else than a wall or an item (probably a level wall) - return false; + if (pickedBody.UserData is ISpatialEntity e) + { + Submarine sub = e.Submarine; + if (sub == null) { return false; } + if (sub == Item.Submarine) { return false; } + // Don't shoot non-player submarines, i.e. wrecks or outposts. + if (!sub.Info.IsPlayer) { return false; } + // Don't shoot friendly submarines. + if (sub.TeamID == Item.Submarine.TeamID) { return false; } + } + else + { + // Hit something else, probably a level wall + return false; + } } - - if (objective.Option.ToLowerInvariant() == "fireatwill") + if (objective.Option.Equals("fireatwill", StringComparison.OrdinalIgnoreCase)) { character?.Speak(TextManager.GetWithVariable("DialogFireTurret", "[itemname]", item.Name, true), null, 0.0f, "fireturret", 5.0f); character.SetInput(InputType.Shoot, true, true); } - return false; } - private void GetAvailablePower(out float availableCharge, out float availableCapacity) - { - var batteries = item.GetConnectedComponents(); - - availableCharge = 0.0f; - availableCapacity = 0.0f; - foreach (PowerContainer battery in batteries) - { - availableCharge += battery.Charge; - availableCapacity += battery.Capacity; - } - } - protected override void RemoveComponentSpecific() { base.RemoveComponentSpecific(); @@ -614,7 +906,7 @@ namespace Barotrauma.Items.Components else { //check if the contained item is another itemcontainer with projectiles inside it - if (containedItem.ContainedItems == null) continue; + if (containedItem.ContainedItems == null) { continue; } foreach (Item subContainedItem in containedItem.ContainedItems) { projectileComponent = subContainedItem.GetComponent(); @@ -695,7 +987,6 @@ namespace Barotrauma.Items.Components TryLaunch((float)Timing.Step, sender); } break; - case "toggle": case "toggle_light": if (lightComponent != null) { @@ -707,8 +998,25 @@ namespace Barotrauma.Items.Components public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null) { - Item item = (Item)extraData[2]; - msg.Write(item.Removed ? (ushort)0 : item.ID); + if (extraData.Length > 2) + { + msg.Write(!(extraData[2] is Item item) || item.Removed ? ushort.MaxValue : item.ID); + msg.WriteRangedSingle(MathHelper.Clamp(rotation, minRotation, maxRotation), minRotation, maxRotation, 16); + } + else + { + msg.Write((ushort)0); + float wrappedTargetRotation = targetRotation; + while (wrappedTargetRotation < minRotation && MathUtils.IsValid(wrappedTargetRotation)) + { + wrappedTargetRotation += MathHelper.TwoPi; + } + while (wrappedTargetRotation > maxRotation && MathUtils.IsValid(wrappedTargetRotation)) + { + wrappedTargetRotation -= MathHelper.TwoPi; + } + msg.WriteRangedSingle(MathHelper.Clamp(wrappedTargetRotation, minRotation, maxRotation), minRotation, maxRotation, 16); + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index df34a4c9a..c95adedb1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -211,6 +211,7 @@ namespace Barotrauma.Items.Components } public bool AutoEquipWhenFull { get; private set; } + public bool DisplayContainedStatus { get; private set; } public readonly int Variants; @@ -264,6 +265,7 @@ namespace Barotrauma.Items.Components limbType = new LimbType[spriteCount]; limb = new Limb[spriteCount]; AutoEquipWhenFull = element.GetAttributeBool("autoequipwhenfull", true); + DisplayContainedStatus = element.GetAttributeBool("displaycontainedstatus", false); int i = 0; foreach (XElement subElement in element.Elements()) { @@ -284,7 +286,7 @@ namespace Barotrauma.Items.Components foreach (XElement lightElement in subElement.Elements()) { - if (lightElement.Name.ToString().ToLowerInvariant() != "lightcomponent") continue; + if (!lightElement.Name.ToString().Equals("lightcomponent", StringComparison.OrdinalIgnoreCase)) { continue; } wearableSprites[i].LightComponent = new LightComponent(item, lightElement) { Parent = this diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index 2a46248ef..c54233341 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -54,6 +54,33 @@ namespace Barotrauma #endif } + public static Item FindItemRecursive(Item item, Predicate condition) + { + if (condition.Invoke(item)) + { + return item; + } + + var containers = item.GetComponents(); + + if (containers != null) + { + foreach (var container in containers) + { + foreach (var inventoryItem in container.Inventory.Items) + { + var findItem = FindItemRecursive(inventoryItem, condition); + if (findItem != null) + { + return findItem; + } + } + } + } + + return null; + } + public int FindIndex(Item item) { for (int i = 0; i < capacity; i++) @@ -173,6 +200,7 @@ namespace Barotrauma if (Owner == null) return; Inventory prevInventory = item.ParentInventory; + Inventory prevOwnerInventory = item.FindParentInventory(inv => inv is CharacterInventory); if (createNetworkEvent) { @@ -197,7 +225,16 @@ namespace Barotrauma if (item.body != null) { item.body.Enabled = false; + item.body.BodyType = FarseerPhysics.BodyType.Dynamic; } + +#if SERVER + if (prevOwnerInventory is CharacterInventory characterInventory && characterInventory != this && Owner == user) + { + var client = GameMain.Server?.ConnectedClients?.Find(cl => cl.Character == user); + GameMain.Server?.KarmaManager.OnItemTakenFromPlayer(characterInventory, client, item); + } +#endif } public bool IsEmpty() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index be96b65b4..790e618f7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -25,7 +25,8 @@ namespace Barotrauma OnFire, InWater, NotInWater, OnImpact, OnEating, - OnDeath = OnBroken + OnDeath = OnBroken, + OnDamaged } partial class Item : MapEntity, IDamageable, ISerializableEntity, IServerSerializable, IClientSerializable @@ -37,6 +38,8 @@ namespace Barotrauma private HashSet tags; + private bool isWire; + private Hull currentHull; public Hull CurrentHull { @@ -62,11 +65,14 @@ namespace Barotrauma /// private readonly List updateableComponents = new List(); private List drawableComponents; + private bool hasComponentsToDraw; public PhysicsBody body; public readonly XElement StaticBodyConfig; + public List StaticFixtures = new List(); + private bool transformDirty = true; private float lastSentCondition; @@ -164,13 +170,6 @@ namespace Barotrauma get { return description ?? prefab.Description; } set { description = value; } } - - [Editable, Serialize(false, true)] - public bool HiddenInGame - { - get; - set; - } [Editable, Serialize(false, true)] public bool NonInteractable @@ -290,6 +289,24 @@ namespace Barotrauma set { /*do nothing*/ } } + + [Serialize("", true)] + + /// + /// Can be used to modify the AITarget's label using status effects + /// + public string SonarLabel + { + get { return AiTarget?.SonarLabel ?? ""; } + set + { + if (AiTarget != null) + { + AiTarget.SonarLabel = value; + } + } + } + [Serialize(false, false)] /// /// Can be used by status effects or conditionals to check if the physics body of the item is active @@ -586,7 +603,7 @@ namespace Barotrauma spriteColor = prefab.SpriteColor; components = new List(); - drawableComponents = new List(); + drawableComponents = new List(); hasComponentsToDraw = false; tags = new HashSet(); repairables = new List(); @@ -614,7 +631,7 @@ namespace Barotrauma case "body": body = new PhysicsBody(subElement, ConvertUnits.ToSimUnits(Position), Scale); string collisionCategory = subElement.GetAttributeString("collisioncategory", null); - if (Prefab.DamagedByProjectiles || Prefab.DamagedByMeleeWeapons) + if ((Prefab.DamagedByProjectiles || Prefab.DamagedByMeleeWeapons) && Condition > 0) { //force collision category to Character to allow projectiles and weapons to hit //(we could also do this by making the projectiles and weapons hit CollisionItem @@ -671,7 +688,11 @@ namespace Barotrauma AddComponent(ic); - if (ic is IDrawableComponent && ic.Drawable) drawableComponents.Add(ic as IDrawableComponent); + if (ic is IDrawableComponent && ic.Drawable) + { + drawableComponents.Add(ic as IDrawableComponent); + hasComponentsToDraw = true; + } if (ic is Repairable) repairables.Add((Repairable)ic); break; } @@ -750,6 +771,8 @@ namespace Barotrauma ItemList.Add(this); DebugConsole.Log("Created " + Name + " (" + ID + ")"); + + if (Components.All(ic => ic is Wire || ic is Holdable)) { isWire = true; } } partial void InitProjSpecific(); @@ -876,12 +899,23 @@ namespace Barotrauma if (!drawableComponents.Contains(drawable)) { drawableComponents.Add(drawable); + hasComponentsToDraw = true; +#if CLIENT + cachedVisibleSize = null; +#endif } } public void DisableDrawableComponent(IDrawableComponent drawable) { - drawableComponents.Remove(drawable); + if (drawableComponents.Contains(drawable)) + { + drawableComponents.Remove(drawable); + hasComponentsToDraw = drawableComponents.Count > 0; +#if CLIENT + cachedVisibleSize = null; +#endif + } } public int GetComponentIndex(ItemComponent component) @@ -1092,16 +1126,28 @@ namespace Barotrauma public void AddTag(string tag) { - if (tags.Contains(tag)) return; + if (tags.Contains(tag)) { return; } tags.Add(tag); } public bool HasTag(string tag) { - if (tag == null) return true; + if (tag == null) { return true; } return tags.Contains(tag) || prefab.Tags.Contains(tag); } + public void ReplaceTag(string tag, string newTag) + { + if (!tags.Contains(tag)) { return; } + tags.Remove(tag); + tags.Add(newTag); + } + + public IEnumerable GetTags() + { + return tags; + } + public bool HasTag(IEnumerable allowedTags) { if (allowedTags == null) return true; @@ -1237,6 +1283,8 @@ namespace Barotrauma float damageAmount = attack.GetItemDamage(deltaTime); Condition -= damageAmount; + ApplyStatusEffects(ActionType.OnDamaged, 1.0f); + return new AttackResult(damageAmount, null); } @@ -1296,6 +1344,7 @@ namespace Barotrauma #if CLIENT if (ic.HasSounds) { + ic.PlaySound(ActionType.Always); ic.UpdateSounds(); if (!ic.WasUsed) { @@ -1376,7 +1425,7 @@ namespace Barotrauma } else { - if (updateableComponents.Count == 0 && aiTarget == null && !hasStatusEffectsOfType[(int)ActionType.Always] && body == null) + if (updateableComponents.Count == 0 && aiTarget == null && !hasStatusEffectsOfType[(int)ActionType.Always] && (body == null || !body.Enabled)) { #if CLIENT positionBuffer.Clear(); @@ -1726,16 +1775,15 @@ namespace Barotrauma public bool TryInteract(Character picker, bool ignoreRequiredItems = false, bool forceSelectKey = false, bool forceActionKey = false) { - bool hasRequiredSkills = true; - bool picked = false, selected = false; - +#if CLIENT + bool hasRequiredSkills = true; Skill requiredSkill = null; - +#endif foreach (ItemComponent ic in components) { bool pickHit = false, selectHit = false; - + if (picker.IsKeyDown(InputType.Aim)) { pickHit = false; @@ -1779,13 +1827,11 @@ namespace Barotrauma picker.IsKeyHit(InputType.Select); } #endif - - if (!pickHit && !selectHit) continue; - - if (!ic.HasRequiredSkills(picker, out Skill tempRequiredSkill)) hasRequiredSkills = false; + if (!pickHit && !selectHit) { continue; } bool showUiMsg = false; #if CLIENT + if (!ic.HasRequiredSkills(picker, out Skill tempRequiredSkill)) { hasRequiredSkills = false; } showUiMsg = picker == Character.Controlled && Screen.Selected != GameMain.SubEditorScreen; #endif if (!ignoreRequiredItems && !ic.HasRequiredItems(picker, showUiMsg)) continue; @@ -1795,10 +1841,9 @@ namespace Barotrauma picked = true; ic.ApplyStatusEffects(ActionType.OnPicked, 1.0f, picker); #if CLIENT - if (picker == Character.Controlled) GUI.ForceMouseOn(null); + if (picker == Character.Controlled) { GUI.ForceMouseOn(null); } + if (tempRequiredSkill != null) { requiredSkill = tempRequiredSkill; } #endif - if (tempRequiredSkill != null) requiredSkill = tempRequiredSkill; - if (ic.CanBeSelected) selected = true; } } @@ -1839,6 +1884,30 @@ namespace Barotrauma return true; } + public float GetContainedItemConditionPercentage() + { + var containedItems = ContainedItems; + + if (containedItems != null) + { + float condition = 0f; + float maxCondition = 0f; + + foreach (Item item in containedItems) + { + condition += item.condition; + maxCondition += item.MaxCondition; + } + + if (maxCondition > 0.0f) + { + return condition / maxCondition; + } + } + + return -1; + } + public void Use(float deltaTime, Character character = null, Limb targetLimb = null) { if (RequireAimToUse && (character == null || !character.IsKeyDown(InputType.Aim))) @@ -1968,6 +2037,8 @@ namespace Barotrauma public void Drop(Character dropper, bool createNetworkEvent = true) { + Inventory prevInventory = parentInventory; + if (createNetworkEvent) { if (parentInventory != null && !parentInventory.Owner.Removed && !Removed && @@ -1981,6 +2052,7 @@ namespace Barotrauma if (body != null) { + isActive = true; body.Enabled = true; body.PhysEnabled = true; body.ResetDynamics(); @@ -2333,9 +2405,9 @@ namespace Barotrauma item.SetActiveSprite(); - if (submarine?.GameVersion != null) + if (submarine?.Info.GameVersion != null) { - SerializableProperty.UpgradeGameVersion(item, item.Prefab.ConfigElement, submarine.GameVersion); + SerializableProperty.UpgradeGameVersion(item, item.Prefab.ConfigElement, submarine.Info.GameVersion); } foreach (ItemComponent component in item.components) @@ -2442,7 +2514,7 @@ namespace Barotrauma return; } DebugConsole.Log("Removing item " + Name + " (ID: " + ID + ")"); - + base.Remove(); foreach (Character character in Character.CharacterList) @@ -2472,6 +2544,18 @@ namespace Barotrauma body = null; } + if (StaticFixtures != null) + { + foreach (Fixture fixture in StaticFixtures) + { + //if the world is null, the body has already been removed + //happens if the sub the fixture is attached to is removed before the item + if (fixture.Body?.World == null) { continue; } + fixture.Body.Remove(fixture); + } + StaticFixtures.Clear(); + } + foreach (Item it in ItemList) { if (it.linkedTo.Contains(this)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index 041b021ad..2b89250fa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -255,6 +255,13 @@ namespace Barotrauma private set; } + //if true and the item has trigger areas defined, players can only highlight the item when the cursor is on the trigger + [Serialize(false, false)] + public bool RequireCursorInsideTrigger + { + get; + private set; + } //should the camera focus on the item when selected [Serialize(false, false)] @@ -307,6 +314,20 @@ namespace Barotrauma private set; } + [Serialize(false, false)] + public bool DamagedByRepairTools + { + get; + private set; + } + + [Serialize(false, false)] + public bool DamagedByMonsters + { + get; + private set; + } + [Serialize(false, false)] public bool FireProof { @@ -535,6 +556,8 @@ namespace Barotrauma } Category = category; + var parentType = element.Parent?.GetAttributeString("itemtype", "") ?? string.Empty; + //nameidentifier can be used to make multiple items use the same names and descriptions string nameIdentifier = element.GetAttributeString("nameidentifier", ""); @@ -566,7 +589,15 @@ namespace Barotrauma identifier = GenerateLegacyIdentifier(originalName); } } - + + if (string.Equals(parentType, "wrecked", StringComparison.OrdinalIgnoreCase)) + { + if (!string.IsNullOrEmpty(name)) + { + name = TextManager.GetWithVariable("wreckeditemformat", "[name]", name); + } + } + if (string.IsNullOrEmpty(name)) { DebugConsole.ThrowError($"Unnamed item ({identifier})in {filePath}!"); @@ -810,7 +841,7 @@ namespace Barotrauma public PriceInfo GetPrice(Location location) { - if (prices == null || !prices.ContainsKey(location.Type.Identifier.ToLowerInvariant())) return null; + if (prices == null || !prices.ContainsKey(location.Type.Identifier.ToLowerInvariant())) { return null; } return prices[location.Type.Identifier.ToLowerInvariant()]; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs index a0faf465e..4cc0bc78c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs @@ -219,7 +219,10 @@ namespace Barotrauma string typeStr = element.GetAttributeString("type", ""); if (string.IsNullOrEmpty(typeStr)) { - if (element.Name.ToString().ToLowerInvariant() == "containable") typeStr = "Contained"; + if (element.Name.ToString().Equals("containable", StringComparison.OrdinalIgnoreCase)) + { + typeStr = "Contained"; + } } if (!Enum.TryParse(typeStr, true, out ri.type)) { @@ -246,7 +249,7 @@ namespace Barotrauma foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "statuseffect") continue; + if (!subElement.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase)) { continue; } ri.statusEffects.Add(StatusEffect.Load(subElement, parentDebugName)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/CorpsePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/CorpsePrefab.cs new file mode 100644 index 000000000..1e0d220d2 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/CorpsePrefab.cs @@ -0,0 +1,228 @@ +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Barotrauma +{ + class CorpsePrefab : IPrefab, IDisposable + { + public static readonly PrefabCollection Prefabs = new PrefabCollection(); + + private bool disposed = false; + public void Dispose() + { + if (disposed) { return; } + disposed = true; + Prefabs.Remove(this); + } + + public static CorpsePrefab Get(string identifier) + { + if (Prefabs == null) + { + DebugConsole.ThrowError("Issue in the code execution order: job prefabs not loaded."); + return null; + } + if (Prefabs.ContainsKey(identifier)) + { + return Prefabs[identifier]; + } + else + { + DebugConsole.ThrowError("Couldn't find a job prefab with the given identifier: " + identifier); + return null; + } + } + + [Serialize("notfound", false)] + public string Identifier { get; private set; } + + [Serialize("any", false)] + public string Job { get; private set; } + + [Serialize(1f, false)] + public float Commonness { get; private set; } + + [Serialize(Level.PositionType.Wreck, false)] + public Level.PositionType SpawnPosition { get; private set; } + + public string OriginalName { get { return Identifier; } } + + public ContentPackage ContentPackage { get; private set; } + + public string FilePath { get; private set; } + + public XElement Element { get; private set; } + + public readonly Dictionary ItemSets = new Dictionary(); + + public CorpsePrefab(XElement element, string filePath, bool allowOverriding) + { + FilePath = filePath; + SerializableProperty.DeserializeProperties(this, element); + Identifier = Identifier.ToLowerInvariant(); + Job = Job.ToLowerInvariant(); + Element = element; + element.GetChildElements("itemset").ForEach(e => ItemSets.Add(e, e.GetAttributeFloat("commonness", 1))); + Prefabs.Add(this, allowOverriding); + } + + public static CorpsePrefab Random(Rand.RandSync sync = Rand.RandSync.Unsynced) => Prefabs.GetRandom(sync); + + public static void LoadAll(IEnumerable files) + { + foreach (ContentFile file in files) + { + LoadFromFile(file); + } + } + + public static void LoadFromFile(ContentFile file) + { + DebugConsole.Log("*** " + file.Path + " ***"); + RemoveByFile(file.Path); + + XDocument doc = XMLExtensions.TryLoadXml(file.Path); + if (doc == null) { return; } + + var rootElement = doc.Root; + switch (rootElement.Name.ToString().ToLowerInvariant()) + { + case "corpse": + new CorpsePrefab(rootElement, file.Path, false) + { + ContentPackage = file.ContentPackage + }; + break; + case "corpses": + foreach (var element in rootElement.Elements()) + { + if (element.IsOverride()) + { + var itemElement = element.GetChildElement("item"); + if (itemElement != null) + { + new CorpsePrefab(itemElement, file.Path, true) + { + ContentPackage = file.ContentPackage + }; + } + else + { + DebugConsole.ThrowError($"Cannot find an item element from the children of the override element defined in {file.Path}"); + } + } + else + { + new CorpsePrefab(element, file.Path, false) + { + ContentPackage = file.ContentPackage + }; + } + } + break; + case "override": + var corpses = rootElement.GetChildElement("corpses"); + if (corpses != null) + { + foreach (var element in corpses.Elements()) + { + new CorpsePrefab(element, file.Path, true) + { + ContentPackage = file.ContentPackage, + }; + } + } + foreach (var element in rootElement.GetChildElements("corpse")) + { + new CorpsePrefab(element, file.Path, true) + { + ContentPackage = file.ContentPackage + }; + } + break; + default: + DebugConsole.ThrowError($"Invalid XML root element: '{rootElement.Name.ToString()}' in {file.Path}"); + break; + } + } + + public static void RemoveByFile(string filePath) + { + Prefabs.RemoveByFile(filePath); + } + + public void GiveItems(Character character, Submarine submarine) + { + var spawnItems = ToolBox.SelectWeightedRandom(ItemSets.Keys.ToList(), ItemSets.Values.ToList(), Rand.RandSync.Unsynced); + foreach (XElement itemElement in spawnItems.GetChildElements("item")) + { + InitializeItems(character, itemElement, submarine); + } + } + + private void InitializeItems(Character character, XElement itemElement, Submarine submarine, Item parentItem = null) + { + ItemPrefab itemPrefab; + string itemIdentifier = itemElement.GetAttributeString("identifier", ""); + itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab; + if (itemPrefab == null) + { + DebugConsole.ThrowError("Tried to spawn \"" + Identifier + "\" with the item \"" + itemIdentifier + "\". Matching item prefab not found."); + return; + } + Item item = new Item(itemPrefab, character.Position, null); +#if SERVER + if (GameMain.Server != null && Entity.Spawner != null) + { + if (GameMain.Server.EntityEventManager.UniqueEvents.Any(ev => ev.Entity == item)) + { + string errorMsg = $"Error while spawning job items. Item {item.Name} created network events before the spawn event had been created."; + DebugConsole.ThrowError(errorMsg); + GameAnalyticsManager.AddErrorEventOnce("Job.InitializeJobItem:EventsBeforeSpawning", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); + GameMain.Server.EntityEventManager.UniqueEvents.RemoveAll(ev => ev.Entity == item); + GameMain.Server.EntityEventManager.Events.RemoveAll(ev => ev.Entity == item); + } + + Entity.Spawner.CreateNetworkEvent(item, false); + } +#endif + if (itemElement.GetAttributeBool("equip", false)) + { + List allowedSlots = new List(item.AllowedSlots); + allowedSlots.Remove(InvSlotType.Any); + + character.Inventory.TryPutItem(item, null, allowedSlots); + } + else + { + character.Inventory.TryPutItem(item, null, item.AllowedSlots); + } + if (item.Prefab.Identifier == "idcard" || item.Prefab.Identifier == "idcardwreck") + { + item.AddTag("name:" + character.Name); + item.ReplaceTag("wreck_id", Level.Loaded.GetWreckIDTag("wreck_id", submarine)); + var job = character.Info?.Job; + if (job != null) + { + item.AddTag("job:" + job.Name); + } + } + foreach (WifiComponent wifiComponent in item.GetComponents()) + { + wifiComponent.TeamID = character.TeamID; + } + if (parentItem != null) + { + parentItem.Combine(item, user: null); + } + foreach (XElement childItemElement in itemElement.Elements()) + { + InitializeItems(character, childItemElement, submarine, item); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index b69253f6f..34b4f47f9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -30,9 +30,9 @@ namespace Barotrauma public float EmpStrength { get; set; } - public Explosion(float range, float force, float damage, float structureDamage, float empStrength = 0.0f) + public Explosion(float range, float force, float damage, float structureDamage, float itemDamage, float empStrength = 0.0f) { - attack = new Attack(damage, 0.0f, 0.0f, structureDamage, range) + attack = new Attack(damage, 0.0f, 0.0f, structureDamage, itemDamage, range) { SeverLimbsProbability = 1.0f }; @@ -220,7 +220,7 @@ namespace Barotrauma Vector2 explosionPos = worldPosition; if (c.Submarine != null) { explosionPos -= c.Submarine.Position; } - Hull hull = Hull.FindHull(ConvertUnits.ToDisplayUnits(explosionPos), null, false); + Hull hull = Hull.FindHull(explosionPos, null, false); bool underWater = hull == null || explosionPos.Y < hull.Surface; explosionPos = ConvertUnits.ToSimUnits(explosionPos); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs index 50f3b9554..08feb0f1c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/FireSource.cs @@ -14,7 +14,7 @@ namespace Barotrauma partial class FireSource : ISpatialEntity { const float OxygenConsumption = 50.0f; - const float GrowSpeed = 5.0f; + const float GrowSpeed = 20.0f; protected Hull hull; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs index 454068b5e..57ba7a095 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs @@ -128,8 +128,9 @@ namespace Barotrauma outsideCollisionBlocker.CollisionCategories = Physics.CollisionWall; outsideCollisionBlocker.CollidesWith = Physics.CollisionCharacter; outsideCollisionBlocker.Enabled = false; +#if CLIENT Resized += newRect => IsHorizontal = newRect.Width < newRect.Height; - +#endif DebugConsole.Log("Created gap (" + ID + ")"); } @@ -226,8 +227,6 @@ namespace Barotrauma if (hulls[i] == null) hulls[i] = Hull.FindHullOld(searchPos[i], null, false, true); } - if (hulls[1] == hulls[0]) { hulls[1] = null; } - if (hulls[0] == null && hulls[1] == null) { return; } if (hulls[0] == null && hulls[1] != null) @@ -241,7 +240,7 @@ namespace Barotrauma for (int i = 0; i < 2; i++) { - if (hulls[i] == null) continue; + if (hulls[i] == null) { continue; } linkedTo.Add(hulls[i]); if (!hulls[i].ConnectedGaps.Contains(this)) hulls[i].ConnectedGaps.Add(this); } @@ -259,17 +258,21 @@ namespace Barotrauma return; } - UpdateOxygen(); + Hull hull1 = (Hull)linkedTo[0]; + Hull hull2 = linkedTo.Count < 2 ? null : (Hull)linkedTo[1]; + if (hull1 == hull2) { return; } + + UpdateOxygen(hull1, hull2); if (linkedTo.Count == 1) { //gap leading from a room to outside - UpdateRoomToOut(deltaTime); + UpdateRoomToOut(deltaTime, hull1); } - else + else if (linkedTo.Count == 2) { //gap leading from a room to another - UpdateRoomToRoom(deltaTime); + UpdateRoomToRoom(deltaTime, hull1, hull2); } flowForce.X = MathHelper.Clamp(flowForce.X, -MaxFlowForce, MaxFlowForce); @@ -329,12 +332,8 @@ namespace Barotrauma partial void EmitParticles(float deltaTime); - void UpdateRoomToRoom(float deltaTime) + void UpdateRoomToRoom(float deltaTime, Hull hull1, Hull hull2) { - if (linkedTo.Count < 2) return; - Hull hull1 = (Hull)linkedTo[0]; - Hull hull2 = (Hull)linkedTo[1]; - Vector2 subOffset = Vector2.Zero; if (hull1.Submarine != Submarine) { @@ -378,7 +377,7 @@ namespace Barotrauma delta = Math.Min(((hull2.Pressure + subOffset.Y) - hull1.Pressure) * 5.0f * sizeModifier, Math.Min(hull2.WaterVolume, hull2.Volume)); //make sure not to place more water to the target room than it can hold - delta = Math.Min(delta, hull1.Volume + Hull.MaxCompress - (hull1.WaterVolume)); + delta = Math.Min(delta, hull1.Volume * Hull.MaxCompress - (hull1.WaterVolume)); hull1.WaterVolume += delta; hull2.WaterVolume -= delta; if (hull1.WaterVolume > hull1.Volume) @@ -399,7 +398,7 @@ namespace Barotrauma delta = Math.Min((hull1.Pressure - (hull2.Pressure + subOffset.Y)) * 5.0f * sizeModifier, Math.Min(hull1.WaterVolume, hull1.Volume)); //make sure not to place more water to the target room than it can hold - delta = Math.Min(delta, hull2.Volume + Hull.MaxCompress - (hull2.WaterVolume)); + delta = Math.Min(delta, hull2.Volume * Hull.MaxCompress - (hull2.WaterVolume)); hull1.WaterVolume -= delta; hull2.WaterVolume += delta; if (hull2.WaterVolume > hull2.Volume) @@ -414,14 +413,14 @@ namespace Barotrauma { float avg = (hull1.Surface + hull2.Surface) / 2.0f; - if (hull1.WaterVolume < hull1.Volume - Hull.MaxCompress && + if (hull1.WaterVolume < hull1.Volume / Hull.MaxCompress && hull1.Surface + hull1.WaveY[hull1.WaveY.Length - 1] < rect.Y) { hull1.WaveVel[hull1.WaveY.Length - 1] = (avg - (hull1.Surface + hull1.WaveY[hull1.WaveY.Length - 1])) * 0.1f; hull1.WaveVel[hull1.WaveY.Length - 2] = hull1.WaveVel[hull1.WaveY.Length - 1]; } - if (hull2.WaterVolume < hull2.Volume - Hull.MaxCompress && + if (hull2.WaterVolume < hull2.Volume / Hull.MaxCompress && hull2.Surface + hull2.WaveY[0] < rect.Y) { hull2.WaveVel[0] = (avg - (hull2.Surface + hull2.WaveY[0])) * 0.1f; @@ -436,12 +435,12 @@ namespace Barotrauma //lower room is full of water if (hull2.Pressure + subOffset.Y > hull1.Pressure && hull2.WaterVolume > 0.0f) { - float delta = Math.Min(hull2.WaterVolume - hull2.Volume + Hull.MaxCompress, deltaTime * 8000.0f * sizeModifier); + float delta = Math.Min(hull2.WaterVolume - hull2.Volume + (hull2.Volume * Hull.MaxCompress), deltaTime * 8000.0f * sizeModifier); //make sure not to place more water to the target room than it can hold - if (hull1.WaterVolume + delta > hull1.Volume + Hull.MaxCompress) + if (hull1.WaterVolume + delta > hull1.Volume * Hull.MaxCompress) { - delta -= (hull1.WaterVolume + delta) - (hull1.Volume + Hull.MaxCompress); + delta -= (hull1.WaterVolume + delta) - (hull1.Volume * Hull.MaxCompress); } delta = Math.Max(delta, 0.0f); @@ -469,9 +468,9 @@ namespace Barotrauma float delta = Math.Min(hull1.WaterVolume, deltaTime * 25000f * sizeModifier); //make sure not to place more water to the target room than it can hold - if (hull2.WaterVolume + delta > hull2.Volume + Hull.MaxCompress) + if (hull2.WaterVolume + delta > hull2.Volume * Hull.MaxCompress) { - delta -= (hull2.WaterVolume + delta) - (hull2.Volume + Hull.MaxCompress); + delta -= (hull2.WaterVolume + delta) - (hull2.Volume * Hull.MaxCompress); } hull1.WaterVolume -= delta; hull2.WaterVolume += delta; @@ -489,7 +488,7 @@ namespace Barotrauma if (open > 0.0f) { - if (hull1.WaterVolume > hull1.Volume - Hull.MaxCompress && hull2.WaterVolume > hull2.Volume - Hull.MaxCompress) + if (hull1.WaterVolume > hull1.Volume / Hull.MaxCompress && hull2.WaterVolume > hull2.Volume / Hull.MaxCompress) { float avgLethality = (hull1.LethalPressure + hull2.LethalPressure) / 2.0f; hull1.LethalPressure = avgLethality; @@ -503,22 +502,18 @@ namespace Barotrauma } } - void UpdateRoomToOut(float deltaTime) + void UpdateRoomToOut(float deltaTime, Hull hull1) { - if (linkedTo.Count != 1) return; - - float size = (IsHorizontal) ? rect.Height : rect.Width; - - Hull hull1 = (Hull)linkedTo[0]; + float size = IsHorizontal ? rect.Height : rect.Width; //a variable affecting the water flow through the gap //the larger the gap is, the faster the water flows float sizeModifier = size * open * open; - float delta = Hull.MaxCompress * sizeModifier * deltaTime; + float delta = 500.0f * sizeModifier * deltaTime; //make sure not to place more water to the target room than it can hold - delta = Math.Min(delta, hull1.Volume + Hull.MaxCompress - hull1.WaterVolume); + delta = Math.Min(delta, hull1.Volume * Hull.MaxCompress - hull1.WaterVolume); hull1.WaterVolume += delta; if (hull1.WaterVolume > hull1.Volume) hull1.Pressure += 0.5f; @@ -541,7 +536,7 @@ namespace Barotrauma higherSurface = hull1.Surface; lowerSurface = rect.Y; - if (hull1.WaterVolume < hull1.Volume - Hull.MaxCompress && + if (hull1.WaterVolume < hull1.Volume / Hull.MaxCompress && hull1.Surface < rect.Y) { if (rect.X > hull1.Rect.X + hull1.Rect.Width / 2.0f) @@ -576,7 +571,7 @@ namespace Barotrauma { flowForce = new Vector2(0.0f, delta); } - if (hull1.WaterVolume >= hull1.Volume - Hull.MaxCompress) + if (hull1.WaterVolume >= hull1.Volume / Hull.MaxCompress) { hull1.LethalPressure += (Submarine != null && Submarine.AtDamageDepth) ? 100.0f * deltaTime : 10.0f * deltaTime; } @@ -600,7 +595,7 @@ namespace Barotrauma private void UpdateOutsideColliderPos(Hull hull) { - if (Submarine == null || IsRoomToRoom) { return; } + if (Submarine == null || IsRoomToRoom || Level.Loaded == null) { return; } Vector2 rayDir; if (IsHorizontal) @@ -615,6 +610,19 @@ namespace Barotrauma Vector2 rayStart = ConvertUnits.ToSimUnits(WorldPosition); Vector2 rayEnd = rayStart + rayDir * 500.0f; + var levelCells = Level.Loaded.GetCells(WorldPosition, searchDepth: 1); + foreach (var cell in levelCells) + { + if (cell.IsPointInside(WorldPosition)) + { + outsideCollisionBlocker.Enabled = true; + Vector2 colliderPos = rayStart - Submarine.SimPosition; + float colliderRotation = MathUtils.VectorToAngle(rayDir) - MathHelper.PiOver2; + outsideCollisionBlocker.SetTransformIgnoreContacts(ref colliderPos, colliderRotation); + return; + } + } + var blockingBody = Submarine.CheckVisibility(rayStart, rayEnd); if (blockingBody != null) { @@ -631,11 +639,9 @@ namespace Barotrauma } } - private void UpdateOxygen() + private void UpdateOxygen(Hull hull1, Hull hull2) { - if (linkedTo.Count < 2) { return; } - Hull hull1 = (Hull)linkedTo[0]; - Hull hull2 = (Hull)linkedTo[1]; + if (hull1 == null || hull2 == null) { return; } if (IsHorizontal) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs index 06ff02e92..3de38ad57 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Hull.cs @@ -26,8 +26,9 @@ namespace Barotrauma public static float WaveSpread = 0.05f; public static float WaveDampening = 0.05f; - //how much excess water the room can contain (= more than the volume of the room) - public const float MaxCompress = 10000f; + //how much excess water the room can contain, relative to the volume of the room. + //needed to make it possible for pressure to "push" water up through U-shaped hull configurations + public const float MaxCompress = 1.05f; public readonly Dictionary properties; public Dictionary SerializableProperties @@ -154,7 +155,7 @@ namespace Barotrauma set { if (!MathUtils.IsValid(value)) return; - waterVolume = MathHelper.Clamp(value, 0.0f, Volume + MaxCompress); + waterVolume = MathHelper.Clamp(value, 0.0f, Volume * MaxCompress); if (waterVolume < Volume) Pressure = rect.Y - rect.Height + waterVolume / rect.Width; if (waterVolume > 0.0f) update = true; } @@ -228,7 +229,7 @@ namespace Barotrauma surface = rect.Y - rect.Height; - if (submarine != null) + if (submarine?.Info != null && !submarine.Info.IsWreck) { aiTarget = new AITarget(this) { @@ -321,6 +322,7 @@ namespace Barotrauma CeilingHeight = ConvertUnits.ToDisplayUnits(upperPickedPos.Y - lowerPickedPos.Y); } } + Pressure = rect.Y - rect.Height + waterVolume / rect.Width; } public void AddToGrid(Submarine submarine) @@ -444,15 +446,23 @@ namespace Barotrauma lethalPressure = 0.0f; return; } + + float waterDepth = WaterVolume / rect.Width; + if (waterDepth < 1.0f) + { + //if there's only a minuscule amount of water, consider the surface to be at the bottom of the hull + //otherwise unnoticeable amounts of water can for example cause magnesium to explode + waterDepth = 0.0f; + } surface = Math.Max(MathHelper.Lerp( surface, - rect.Y - rect.Height + WaterVolume / rect.Width, + rect.Y - rect.Height + waterDepth, deltaTime * 10.0f), rect.Y - rect.Height); //interpolate the position of the rendered surface towards the "target surface" drawSurface = Math.Max(MathHelper.Lerp( drawSurface, - rect.Y - rect.Height + WaterVolume / rect.Width, + rect.Y - rect.Height + waterDepth, deltaTime * 10.0f), rect.Y - rect.Height); for (int i = 0; i < waveY.Length; i++) @@ -873,7 +883,7 @@ namespace Barotrauma var hull = new Hull(MapEntityPrefab.Find(null, "hull"), rect, submarine) { - waterVolume = element.GetAttributeFloat("pressure", 0.0f), + WaterVolume = element.GetAttributeFloat("pressure", 0.0f), ID = (ushort)int.Parse(element.Attribute("ID").Value) }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs index 50ca02659..2cd3b6d46 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/ItemAssemblyPrefab.cs @@ -89,7 +89,7 @@ namespace Barotrauma CreateInstance(rect.Location.ToVector2(), Submarine.MainSub); } - public List CreateInstance(Vector2 position, Submarine sub) + public List CreateInstance(Vector2 position, Submarine sub, bool selectPrefabs = false) { List entities = MapEntity.LoadAll(sub, configElement, FilePath); if (entities.Count == 0) return entities; @@ -107,10 +107,10 @@ namespace Barotrauma MapEntity.MapLoaded(entities, true); #if CLIENT - if (Screen.Selected == GameMain.SubEditorScreen) + if (Screen.Selected == GameMain.SubEditorScreen && selectPrefabs) { MapEntity.SelectedList.Clear(); - entities.ForEach(e => MapEntity.AddSelection(e)); + entities.ForEach(MapEntity.AddSelection); } #endif return entities; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index ae5740cec..65970d17f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -27,18 +27,24 @@ namespace Barotrauma [Flags] public enum PositionType { - MainPath = 1, Cave = 2, Ruin = 4 + MainPath = 1, Cave = 2, Ruin = 4, Wreck = 8 } public struct InterestingPosition { public Point Position; public readonly PositionType PositionType; + public bool IsValid; + public Submarine Submarine; + public Ruin Ruin; - public InterestingPosition(Point position, PositionType positionType) + public InterestingPosition(Point position, PositionType positionType, bool isValid = true, Submarine submarine = null, Ruin ruin = null) { Position = position; PositionType = positionType; + IsValid = isValid; + Submarine = submarine; + Ruin = ruin; } } @@ -68,6 +74,8 @@ namespace Barotrauma private List ruins; + private List wrecks; + private LevelGenerationParams generationParams; private List> smallTunnels = new List>(); @@ -133,11 +141,13 @@ namespace Barotrauma get { return positionsOfInterest; } } + public readonly List UsedPositions = new List(); + public Submarine StartOutpost { get; private set; } public Submarine EndOutpost { get; private set; } - private Submarine preSelectedStartOutpost; - private Submarine preSelectedEndOutpost; + private SubmarineInfo preSelectedStartOutpost; + private SubmarineInfo preSelectedEndOutpost; public string Seed { @@ -210,7 +220,7 @@ namespace Barotrauma /// /// A scalar between 0-100 /// A scalar between 0-1 (0 = the minimum width defined in the generation params is used, 1 = the max width is used) - public Level(string seed, float difficulty, float sizeFactor, LevelGenerationParams generationParams, Biome biome, Submarine startOutpost = null, Submarine endOutPost = null) + public Level(string seed, float difficulty, float sizeFactor, LevelGenerationParams generationParams, Biome biome, SubmarineInfo startOutpost = null, SubmarineInfo endOutPost = null) : base(null) { @@ -310,7 +320,7 @@ namespace Barotrauma if (Submarine.MainSub != null) { Rectangle dockedSubBorders = Submarine.MainSub.GetDockedBorders(); - dockedSubBorders.Inflate(dockedSubBorders.Size.ToVector2() * 0.05f); + dockedSubBorders.Inflate(dockedSubBorders.Size.ToVector2() * 0.15f); minWidth = Math.Max(minWidth, Math.Max(dockedSubBorders.Width, dockedSubBorders.Height)); minWidth = Math.Min(minWidth, maxWidth); } @@ -669,7 +679,6 @@ namespace Barotrauma renderer.SetWallVertices(CaveGenerator.GenerateWallShapes(cellsWithBody, this), generationParams.WallColor); #endif - //---------------------------------------------------------------------------------- // create (placeholder) outposts at the start and end of the level //---------------------------------------------------------------------------------- @@ -692,6 +701,12 @@ namespace Barotrauma GenerateSeaFloor(mirror); + //---------------------------------------------------------------------------------- + // create wrecks + //---------------------------------------------------------------------------------- + + CreateWrecks(); + levelObjectManager.PlaceObjects(this, generationParams.LevelObjectAmount); GenerateItems(); @@ -1164,7 +1179,7 @@ namespace Barotrauma return; } string errorMsg = "Failed to find a suitable position for ruins. Level seed: " + seed + - ", ruin size: " + ruinSize + ", selected sub " + (Submarine.MainSub == null ? "none" : Submarine.MainSub.Name); + ", ruin size: " + ruinSize + ", selected sub " + (Submarine.MainSub == null ? "none" : Submarine.MainSub.Info.Name); DebugConsole.ThrowError(errorMsg); GameAnalyticsManager.AddErrorEventOnce("Level.GenerateRuins:PosNotFound", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg); break; @@ -1201,13 +1216,15 @@ namespace Barotrauma ruins.Add(ruin); ruin.RuinShapes.Sort((shape1, shape2) => shape2.DistanceFromEntrance.CompareTo(shape1.DistanceFromEntrance)); + // TODO: autogenerate waypoints inside the ruins and connect them to the main path in multiple places. + // We need the waypoints for the AI navigation and we could use them for spawning the creatures too. int waypointCount = 0; foreach (WayPoint wp in WayPoint.WayPointList) { if (wp.SpawnType != SpawnType.Enemy || wp.Submarine != null) { continue; } if (ruin.RuinShapes.Any(rs => rs.Rect.Contains(wp.WorldPosition))) { - positionsOfInterest.Add(new InterestingPosition(new Point((int)wp.WorldPosition.X, (int)wp.WorldPosition.Y), PositionType.Ruin)); + positionsOfInterest.Add(new InterestingPosition(new Point((int)wp.WorldPosition.X, (int)wp.WorldPosition.Y), PositionType.Ruin, ruin: ruin)); waypointCount++; } } @@ -1215,7 +1232,7 @@ namespace Barotrauma //not enough waypoints inside ruins -> create some spawn positions manually for (int i = 0; i < 4 - waypointCount && i < ruin.RuinShapes.Count; i++) { - positionsOfInterest.Add(new InterestingPosition(ruin.RuinShapes[i].Rect.Center, PositionType.Ruin)); + positionsOfInterest.Add(new InterestingPosition(ruin.RuinShapes[i].Rect.Center, PositionType.Ruin, ruin: ruin)); } foreach (RuinShape ruinShape in ruin.RuinShapes) @@ -1328,8 +1345,6 @@ namespace Barotrauma Vector2 position = Vector2.Zero; - offsetFromWall = ConvertUnits.ToSimUnits(offsetFromWall); - int tries = 0; do { @@ -1398,7 +1413,7 @@ namespace Barotrauma { foreach (Submarine sub in Submarine.Loaded) { - if (sub.IsOutpost) { continue; } + if (sub.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; } farEnoughPositions.RemoveAll(p => Vector2.DistanceSquared(p.Position.ToVector2(), sub.WorldPosition) < minDistFromSubs * minDistFromSubs); } } @@ -1484,8 +1499,10 @@ namespace Barotrauma return cells; } + private readonly List tempCells = new List(); public List GetCells(Vector2 worldPos, int searchDepth = 2) { + tempCells.Clear(); int gridPosX = (int)Math.Floor(worldPos.X / GridCellSize); int gridPosY = (int)Math.Floor(worldPos.Y / GridCellSize); @@ -1495,12 +1512,11 @@ namespace Barotrauma int startY = Math.Max(gridPosY - searchDepth, 0); int endY = Math.Min(gridPosY + searchDepth, cellGrid.GetLength(1) - 1); - List cells = new List(); for (int y = startY; y <= endY; y++) { for (int x = startX; x <= endX; x++) { - cells.AddRange(cellGrid[x, y]); + tempCells.AddRange(cellGrid[x, y]); } } @@ -1508,11 +1524,317 @@ namespace Barotrauma { foreach (VoronoiCell cell in wall.Cells) { - cells.Add(cell); + tempCells.Add(cell); } } - return cells; + return tempCells; + } + + public string GetWreckIDTag(string originalTag, Submarine wreck) + { + string shortSeed = ToolBox.StringToInt(seed + wreck.Info.Name).ToString(); + if (shortSeed.Length > 6) { shortSeed = shortSeed.Substring(0, 6); } + return originalTag + "_" + shortSeed; + } + + // For debugging + private readonly Dictionary> wreckPositions = new Dictionary>(); + private readonly Dictionary> blockedRects = new Dictionary>(); + private void CreateWrecks() + { + var totalSW = new Stopwatch(); + var tempSW = new Stopwatch(); + totalSW.Start(); + var wreckFiles = ContentPackage.GetFilesOfType(GameMain.Config.SelectedContentPackages, ContentType.Wreck).ToList(); + if (wreckFiles.None()) + { + DebugConsole.ThrowError("No wreck files found in the selected content packages!"); + return; + } + wreckFiles.Shuffle(Rand.RandSync.Server); + + int wreckCount = Math.Min(Loaded.GenerationParams.WreckCount, wreckFiles.Count); + // Min distance between a wreck and the start/end/other wreck. + float minDistance = Sonar.DefaultSonarRange; + float squaredMinDistance = minDistance * minDistance; + Vector2 start = startPosition.ToVector2(); + Vector2 end = endPosition.ToVector2(); + var waypoints = WayPoint.WayPointList.Where(wp => + wp.Submarine == null && + wp.SpawnType == SpawnType.Path && + Vector2.DistanceSquared(wp.WorldPosition, start) > squaredMinDistance && + Vector2.DistanceSquared(wp.WorldPosition, end) > squaredMinDistance).ToList(); + wrecks = new List(wreckCount); + for (int i = 0; i < wreckCount; i++) + { + ContentFile contentFile = wreckFiles[i]; + if (contentFile == null) { continue; } + var subDoc = SubmarineInfo.OpenFile(contentFile.Path); + Rectangle borders = Submarine.GetBorders(subDoc.Root); + string wreckName = System.IO.Path.GetFileNameWithoutExtension(contentFile.Path); + // Add some vertical margin so that the wreck doesn't block the path entirely. It's still possible that some larger subs can't pass by. + Point paddedDimensions = new Point(borders.Width, borders.Height + 3000); + tempSW.Restart(); + // For storing the translations. Used only for debugging. + var positions = new List(); + var rects = new List(); + int maxAttempts = 50; + int attemptsLeft = maxAttempts; + bool success = false; + Vector2 spawnPoint = Vector2.Zero; + while (attemptsLeft > 0) + { + if (attemptsLeft < maxAttempts) + { + Debug.WriteLine($"Failed to position the wreck {wreckName}. Trying again."); + } + attemptsLeft--; + if (TryGetSpawnPoint(out spawnPoint)) + { + success = TryPositionWreck(borders, wreckName, ref spawnPoint); + if (success) + { + break; + } + else + { + positions.Clear(); + } + } + else + { + DebugConsole.NewMessage($"Failed to find any spawn point for the wreck: {wreckName} (No valid waypoints left).", Color.Red); + break; + } + } + tempSW.Stop(); + if (success) + { + Debug.WriteLine($"Wreck {wreckName} successfully positioned to {spawnPoint} in {tempSW.ElapsedMilliseconds.ToString()} (ms)"); + tempSW.Restart(); + SubmarineInfo info = new SubmarineInfo(contentFile.Path) + { + Type = SubmarineInfo.SubmarineType.Wreck + }; + Submarine wreck = new Submarine(info); + wreck.MakeWreck(); + tempSW.Stop(); + Debug.WriteLine($"Wreck {wreck.Info.Name} loaded in { tempSW.ElapsedMilliseconds.ToString()} (ms)"); + wrecks.Add(wreck); + wreck.SetPosition(spawnPoint); + wreckPositions.Add(wreck, positions); + blockedRects.Add(wreck, rects); + positionsOfInterest.Add(new InterestingPosition(spawnPoint.ToPoint(), PositionType.Wreck, submarine: wreck)); + foreach (Hull hull in wreck.GetHulls(false)) + { + if (Rand.Value(Rand.RandSync.Server) <= Loaded.GenerationParams.WreckHullFloodingChance) + { + hull.WaterVolume = hull.Volume * Rand.Range(Loaded.GenerationParams.WreckFloodingHullMinWaterPercentage, Loaded.GenerationParams.WreckFloodingHullMaxWaterPercentage, Rand.RandSync.Server); + } + } + if (Rand.Value(Rand.RandSync.Server) <= Loaded.GenerationParams.ThalamusProbability) + { + if (!wreck.CreateWreckAI()) + { + DebugConsole.NewMessage($"Failed to create wreck AI inside {wreckName}.", Color.Red); + wreck.DisableWreckAI(); + } + } + else + { + wreck.DisableWreckAI(); + } + } + else + { + DebugConsole.NewMessage($"Failed to position wreck {wreckName}. Used {tempSW.ElapsedMilliseconds.ToString()} (ms).", Color.Red); + } + + bool TryPositionWreck(Rectangle borders, string wreckName, ref Vector2 spawnPoint) + { + positions.Add(spawnPoint); + bool bottomFound = TryRaycastToBottom(borders, ref spawnPoint); + positions.Add(spawnPoint); + + bool leftSideBlocked = IsSideBlocked(borders, false); + bool rightSideBlocked = IsSideBlocked(borders, true); + int step = 5; + if (rightSideBlocked && !leftSideBlocked) + { + bottomFound = TryMove(borders, ref spawnPoint, -step); + } + else if (leftSideBlocked && !rightSideBlocked) + { + bottomFound = TryMove(borders, ref spawnPoint, step); + } + else if (!bottomFound) + { + if (!leftSideBlocked) + { + bottomFound = TryMove(borders, ref spawnPoint, -step); + } + else if (!rightSideBlocked) + { + bottomFound = TryMove(borders, ref spawnPoint, step); + } + else + { + Debug.WriteLine($"Invalid position {spawnPoint}. Does not touch the ground."); + return false; + } + } + positions.Add(spawnPoint); + bool isBlocked = IsBlocked(spawnPoint, borders.Size - new Point(step + 50)); + if (isBlocked) + { + rects.Add(ToolBox.GetWorldBounds(spawnPoint.ToPoint(), borders.Size)); + Debug.WriteLine($"Invalid position {spawnPoint}. Blocked by level walls."); + } + else if (!bottomFound) + { + Debug.WriteLine($"Invalid position {spawnPoint}. Does not touch the ground."); + } + else + { + var sp = spawnPoint; + if (wrecks.Any(w => Vector2.DistanceSquared(w.WorldPosition, sp) < squaredMinDistance)) + { + Debug.WriteLine($"Invalid position {spawnPoint}. Too close to other wreck(s)."); + return false; + } + } + return !isBlocked && bottomFound; + + bool TryMove(Rectangle borders, ref Vector2 spawnPoint, float amount) + { + float maxMovement = 5000; + float totalAmount = 0; + bool foundBottom = TryRaycastToBottom(borders, ref spawnPoint); + while (!IsSideBlocked(borders, amount > 0)) + { + foundBottom = TryRaycastToBottom(borders, ref spawnPoint); + totalAmount += amount; + spawnPoint = new Vector2(spawnPoint.X + amount, spawnPoint.Y); + if (Math.Abs(totalAmount) > maxMovement) + { + Debug.WriteLine($"Moving the wreck {wreckName} failed."); + break; + } + } + return foundBottom; + } + } + + bool TryGetSpawnPoint(out Vector2 spawnPoint) + { + spawnPoint = Vector2.Zero; + while (waypoints.Any()) + { + var wp = waypoints.GetRandom(Rand.RandSync.Server); + waypoints.Remove(wp); + if (!IsBlocked(wp.WorldPosition, paddedDimensions)) + { + spawnPoint = wp.WorldPosition; + return true; + } + } + return false; + } + + static bool TryRaycastToBottom(Rectangle borders, ref Vector2 spawnPoint) + { + // Shoot five rays and pick the highest hit point. + int rayCount = 5; + var positions = new Vector2[rayCount]; + bool hit = false; + for (int i = 0; i < rayCount; i++) + { + float quarterWidth = borders.Width * 0.25f; + Vector2 rayStart = spawnPoint; + switch (i) + { + case 1: + rayStart = new Vector2(spawnPoint.X - quarterWidth, spawnPoint.Y); + break; + case 2: + rayStart = new Vector2(spawnPoint.X + quarterWidth, spawnPoint.Y); + break; + case 3: + rayStart = new Vector2(spawnPoint.X - quarterWidth / 2, spawnPoint.Y); + break; + case 4: + rayStart = new Vector2(spawnPoint.X + quarterWidth / 2, spawnPoint.Y); + break; + } + var simPos = ConvertUnits.ToSimUnits(rayStart); + var body = Submarine.PickBody(simPos, new Vector2(simPos.X, -1), + customPredicate: f => f.Body?.UserData is VoronoiCell cell && cell.Body.BodyType == BodyType.Static, + collisionCategory: Physics.CollisionLevel | Physics.CollisionWall); + if (body != null) + { + positions[i] = ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition) + new Vector2(0, borders.Height / 2); + hit = true; + } + } + float highestPoint = positions.Max(p => p.Y); + spawnPoint = new Vector2(spawnPoint.X, highestPoint); + return hit; + } + + bool IsSideBlocked(Rectangle borders, bool front) + { + // Shoot three rays and check whether any of them hits. + int rayCount = 3; + Vector2 halfSize = borders.Size.ToVector2() / 2; + Vector2 quarterSize = halfSize / 2; + var positions = new Vector2[rayCount]; + for (int i = 0; i < rayCount; i++) + { + float dir = front ? 1 : -1; + Vector2 rayStart; + Vector2 to; + switch (i) + { + case 1: + rayStart = new Vector2(spawnPoint.X + halfSize.X * dir, spawnPoint.Y + quarterSize.Y); + to = new Vector2(spawnPoint.X + (halfSize.X - quarterSize.X) * dir, rayStart.Y); + break; + case 2: + rayStart = new Vector2(spawnPoint.X + halfSize.X * dir, spawnPoint.Y - quarterSize.Y); + to = new Vector2(spawnPoint.X + (halfSize.X - quarterSize.X) * dir, rayStart.Y); + break; + case 0: + default: + rayStart = spawnPoint; + to = new Vector2(spawnPoint.X + halfSize.X * dir, rayStart.Y); + break; + } + Vector2 simPos = ConvertUnits.ToSimUnits(rayStart); + if (Submarine.PickBody(simPos, ConvertUnits.ToSimUnits(to), + customPredicate: f => f.Body?.UserData is VoronoiCell cell, + collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) != null) + { + return true; + } + } + return false; + } + + bool IsBlocked(Vector2 pos, Point size, float maxDistanceMultiplier = 1) + { + float maxDistance = size.Multiply(maxDistanceMultiplier).ToVector2().LengthSquared(); + Rectangle bounds = ToolBox.GetWorldBounds(pos.ToPoint(), size); + if (ruins.Any(r => ToolBox.GetWorldBounds(r.Area.Center, r.Area.Size).IntersectsWorld(bounds))) + { + return true; + } + var cells = Loaded.GetAllCells().Where(c => c.Body != null && Vector2.DistanceSquared(pos, c.Center) <= maxDistance); + return cells.Any(c => c.BodyVertices.Any(v => bounds.ContainsWorld(v))); + } + } + totalSW.Stop(); + Debug.WriteLine($"{wrecks.Count} wrecks created in { totalSW.ElapsedMilliseconds.ToString()} (ms)"); } private void CreateOutposts() @@ -1537,20 +1859,20 @@ namespace Barotrauma continue; } - Submarine outpost = null; - + SubmarineInfo outpostInfo = null; if (i == 0 && preSelectedStartOutpost == null || i == 1 && preSelectedEndOutpost == null) { string outpostFile = outpostFiles.GetRandom(Rand.RandSync.Server).Path; - outpost = new Submarine(outpostFile, tryLoad: false); + outpostInfo = new SubmarineInfo(outpostFile); } else { - outpost = (i == 0) ? preSelectedStartOutpost : preSelectedEndOutpost; + outpostInfo = (i == 0) ? preSelectedStartOutpost : preSelectedEndOutpost; } - outpost.Load(unloadPrevious: false); - outpost.MakeOutpost(); + outpostInfo.Type = SubmarineInfo.SubmarineType.Outpost; + + var outpost = new Submarine(outpostInfo); Point? minSize = null; DockingPort subPort = null; @@ -1597,9 +1919,9 @@ namespace Barotrauma if (Math.Abs(subDockingPortOffset) > 5000.0f) { subDockingPortOffset = MathHelper.Clamp(subDockingPortOffset, -5000.0f, 5000.0f); - string warningMsg = "Docking port very far from the sub's center of mass (submarine: " + Submarine.MainSub.Name + ", dist: " + subDockingPortOffset + "). The level generator may not be able to place the outpost so that docking is possible."; + string warningMsg = "Docking port very far from the sub's center of mass (submarine: " + Submarine.MainSub.Info.Name + ", dist: " + subDockingPortOffset + "). The level generator may not be able to place the outpost so that docking is possible."; DebugConsole.NewMessage(warningMsg, Color.Orange); - GameAnalyticsManager.AddErrorEventOnce("Lever.CreateOutposts:DockingPortVeryFar" + Submarine.MainSub.Name, GameAnalyticsSDK.Net.EGAErrorSeverity.Warning, warningMsg); + GameAnalyticsManager.AddErrorEventOnce("Lever.CreateOutposts:DockingPortVeryFar" + Submarine.MainSub.Info.Name, GameAnalyticsSDK.Net.EGAErrorSeverity.Warning, warningMsg); } float outpostDockingPortOffset = subPort == null ? 0.0f : outpostPort.Item.WorldPosition.X - outpost.WorldPosition.X; @@ -1607,21 +1929,21 @@ namespace Barotrauma if (Math.Abs(outpostDockingPortOffset) > 5000.0f) { outpostDockingPortOffset = MathHelper.Clamp(outpostDockingPortOffset, -5000.0f, 5000.0f); - string warningMsg = "Docking port very far from the outpost's center of mass (outpost: " + outpost.Name + ", dist: " + outpostDockingPortOffset + "). The level generator may not be able to place the outpost so that docking is possible."; + string warningMsg = "Docking port very far from the outpost's center of mass (outpost: " + outpost.Info.Name + ", dist: " + outpostDockingPortOffset + "). The level generator may not be able to place the outpost so that docking is possible."; DebugConsole.NewMessage(warningMsg, Color.Orange); - GameAnalyticsManager.AddErrorEventOnce("Lever.CreateOutposts:OutpostDockingPortVeryFar" + outpost.Name, GameAnalyticsSDK.Net.EGAErrorSeverity.Warning, warningMsg); + GameAnalyticsManager.AddErrorEventOnce("Lever.CreateOutposts:OutpostDockingPortVeryFar" + outpost.Info.Name, GameAnalyticsSDK.Net.EGAErrorSeverity.Warning, warningMsg); } outpost.SetPosition(outpost.FindSpawnPos(i == 0 ? StartPosition : EndPosition, minSize, subDockingPortOffset - outpostDockingPortOffset)); if ((i == 0) == !Mirrored) { StartOutpost = outpost; - if (GameMain.GameSession?.StartLocation != null) { outpost.Name = GameMain.GameSession.StartLocation.Name; } + if (GameMain.GameSession?.StartLocation != null) { outpost.Info.Name = GameMain.GameSession.StartLocation.Name; } } else { EndOutpost = outpost; - if (GameMain.GameSession?.EndLocation != null) { outpost.Name = GameMain.GameSession.EndLocation.Name; } + if (GameMain.GameSession?.EndLocation != null) { outpost.Info.Name = GameMain.GameSession.EndLocation.Name; } } } } @@ -1635,6 +1957,93 @@ namespace Barotrauma #endif } + public void SpawnCorpses() + { + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + + foreach (Submarine wreck in wrecks) + { + int corpseCount = Rand.Range(Loaded.GenerationParams.MinCorpseCount, Loaded.GenerationParams.MaxCorpseCount); + var allSpawnPoints = WayPoint.WayPointList.FindAll(wp => wp.Submarine == wreck && wp.CurrentHull != null); + var pathPoints = allSpawnPoints.FindAll(wp => wp.SpawnType == SpawnType.Path); + pathPoints.Shuffle(Rand.RandSync.Unsynced); + var corpsePoints = allSpawnPoints.FindAll(wp => wp.SpawnType == SpawnType.Corpse); + corpsePoints.Shuffle(Rand.RandSync.Unsynced); + int spawnCounter = 0; + for (int j = 0; j < corpseCount; j++) + { + WayPoint sp = corpsePoints.FirstOrDefault() ?? pathPoints.FirstOrDefault(); + JobPrefab job = sp?.AssignedJob; + CorpsePrefab selectedPrefab; + if (job == null) + { + // Deduce the job from the selected prefab + selectedPrefab = GetCorpsePrefab(p => p.SpawnPosition == PositionType.Wreck); + job = GetJobPrefab(); + } + else + { + selectedPrefab = GetCorpsePrefab(p => p.SpawnPosition == PositionType.Wreck && (p.Job == "any" || p.Job == job.Identifier)); + if (selectedPrefab == null) + { + corpsePoints.Remove(sp); + pathPoints.Remove(sp); + sp = corpsePoints.FirstOrDefault(sp => sp.AssignedJob == null) ?? pathPoints.FirstOrDefault(sp => sp.AssignedJob == null); + // Deduce the job from the selected prefab + selectedPrefab = GetCorpsePrefab(p => p.SpawnPosition == PositionType.Wreck); + job = GetJobPrefab(); + } + } + if (selectedPrefab == null) { continue; } + Vector2 worldPos; + if (sp == null) + { + if (!TryGetExtraSpawnPoint(out worldPos)) + { + break; + } + job = GetJobPrefab(); + } + else + { + worldPos = sp.WorldPosition; + corpsePoints.Remove(sp); + pathPoints.Remove(sp); + } + if (job == null) { continue; } + var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: job); + var corpse = Character.Create(CharacterPrefab.HumanConfigFile, worldPos, ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true); + corpse.AnimController.FindHull(worldPos, true); + corpse.TeamID = Character.TeamType.None; + corpse.EnableDespawn = false; + selectedPrefab.GiveItems(corpse, wreck); + corpse.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null, log: false); + spawnCounter++; + + static CorpsePrefab GetCorpsePrefab(Func predicate) + { + IEnumerable filteredPrefabs = CorpsePrefab.Prefabs.Where(predicate); + return ToolBox.SelectWeightedRandom(filteredPrefabs.ToList(), filteredPrefabs.Select(p => p.Commonness).ToList(), Rand.RandSync.Unsynced); + } + + JobPrefab GetJobPrefab() => selectedPrefab.Job != null && selectedPrefab.Job != "any" ? JobPrefab.Get(selectedPrefab.Job) : JobPrefab.Random(); + } +#if DEBUG + DebugConsole.NewMessage($"{spawnCounter}/{corpseCount} corpses spawned in {wreck.Info.Name}.", spawnCounter == corpseCount ? Color.Green : Color.Yellow); +#endif + bool TryGetExtraSpawnPoint(out Vector2 point) + { + point = Vector2.Zero; + var hull = Hull.hullList.FindAll(h => h.Submarine == wreck).GetRandom(); + if (hull != null) + { + point = hull.WorldPosition; + } + return hull != null; + } + } + } + public override void Remove() { base.Remove(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs index 463db19dc..97de9243d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelGenerationParams.cs @@ -9,7 +9,6 @@ namespace Barotrauma { class Biome { - public readonly string Identifier; public readonly string DisplayName; public readonly string Description; @@ -105,8 +104,6 @@ namespace Barotrauma private int mountainHeightMin, mountainHeightMax; - private int ruinCount; - private float waterParticleScale; //which biomes can this type of level appear in @@ -338,12 +335,32 @@ namespace Barotrauma } } - [Serialize(1, true, description: "The number of alien ruins in the level."), Editable(MinValueInt = 0, MaxValueInt = 50)] - public int RuinCount - { - get { return ruinCount; } - set { ruinCount = MathHelper.Clamp(value, 0, 10); } - } + [Serialize(1, true, description: "The number of alien ruins in the level."), Editable(MinValueInt = 0, MaxValueInt = 10)] + public int RuinCount { get; set; } + + [Serialize(1, true, description: "The maximum number of wrecks in the level. Note that this value cannot be higher than the amount of wreck prefabs (subs)."), Editable(MinValueInt = 0, MaxValueInt = 10)] + public int WreckCount { get; set; } + + // TODO: Move the wreck parameters under a separate class? +#region Wreck parameters + [Serialize(1, true, description: "The minimum number of corpses per wreck."), Editable(MinValueInt = 0, MaxValueInt = 20)] + public int MinCorpseCount { get; set; } + + [Serialize(5, true, description: "The maximum number of corpses per wreck."), Editable(MinValueInt = 0, MaxValueInt = 20)] + public int MaxCorpseCount { get; set; } + + [Serialize(0.0f, true, description: "How likely is it that a Thalamus inhabits a wreck. Percentage from 0 to 1 per wreck."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] + public float ThalamusProbability { get; set; } + + [Serialize(0.5f, true, description: "How likely the water level of a hull inside a wreck is randomly set."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] + public float WreckHullFloodingChance { get; set; } + + [Serialize(0.1f, true, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] + public float WreckFloodingHullMinWaterPercentage { get; set; } + + [Serialize(1.0f, true, description: "The min water percentage of randomly flooding hulls in wrecks."), Editable(MinValueFloat = 0, MaxValueFloat = 1)] + public float WreckFloodingHullMaxWaterPercentage { get; set; } +#endregion [Serialize(0.4f, true, description: "The probability for wall cells to be removed from the bottom of the map. A value of 0 will produce a completely enclosed tunnel and 1 will make the entire bottom of the level completely open."), Editable()] public float BottomHoleProbability @@ -415,10 +432,10 @@ namespace Barotrauma string biomeName = biomeNames[i].Trim().ToLowerInvariant(); if (biomeName == "none") { continue; } - Biome matchingBiome = biomes.Find(b => b.Identifier.ToLowerInvariant() == biomeName); + Biome matchingBiome = biomes.Find(b => b.Identifier.Equals(biomeName, StringComparison.OrdinalIgnoreCase)); if (matchingBiome == null) { - matchingBiome = biomes.Find(b => b.DisplayName.ToLowerInvariant() == biomeName); + matchingBiome = biomes.Find(b => b.DisplayName.Equals(biomeName, StringComparison.OrdinalIgnoreCase)); if (matchingBiome == null) { DebugConsole.ThrowError("Error in level generation parameters: biome \"" + biomeName + "\" not found."); @@ -487,7 +504,7 @@ namespace Barotrauma mainElement = doc.Root.FirstElement(); biomeElements.Clear(); levelParamElements.Clear(); - DebugConsole.NewMessage($"Overriding the level generation parameters with '{file.Path}'", Color.Yellow); + DebugConsole.NewMessage($"Overriding the level generation parameters and biomes with '{file.Path}'", Color.Yellow); } else if (biomeElements.Any() || levelParamElements.Any()) { @@ -497,7 +514,22 @@ namespace Barotrauma foreach (XElement element in mainElement.Elements()) { - if (element.Name.ToString().ToLowerInvariant() == "biomes") + if (element.IsOverride()) + { + if (element.FirstElement().Name.ToString().Equals("biomes", StringComparison.OrdinalIgnoreCase)) + { + biomeElements.Clear(); + biomeElements.AddRange(element.FirstElement().Elements()); + DebugConsole.NewMessage($"Overriding biomes with '{file.Path}'", Color.Yellow); + } + else + { + levelParamElements.Clear(); + DebugConsole.NewMessage($"Overriding the level generation parameters with '{file.Path}'", Color.Yellow); + levelParamElements.AddRange(element.Elements()); + } + } + else if (element.Name.ToString().Equals("biomes", StringComparison.OrdinalIgnoreCase)) { biomeElements.AddRange(element.Elements()); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelWall.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelWall.cs index 623fed74a..5fdb2ebec 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelWall.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelWall.cs @@ -79,11 +79,13 @@ namespace Barotrauma Vector2[] vertices = new Vector2[4]; vertices[0] = edgePositions[i]; vertices[1] = edgePositions[i + 1]; - vertices[2] = vertices[0] + extendAmount; - vertices[3] = vertices[1] + extendAmount; + vertices[2] = vertices[1] + extendAmount; + vertices[3] = vertices[0] + extendAmount; - VoronoiCell wallCell = new VoronoiCell(vertices); - wallCell.CellType = CellType.Edge; + VoronoiCell wallCell = new VoronoiCell(vertices) + { + CellType = CellType.Edge + }; wallCell.Edges[0].Cell1 = wallCell; wallCell.Edges[1].Cell1 = wallCell; wallCell.Edges[2].Cell1 = wallCell; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs index 14c5f9d6a..4eb5b637f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Ruins/RuinGenerationParams.cs @@ -277,7 +277,7 @@ namespace Barotrauma.RuinGeneration { foreach (XElement subElement in element2.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() == "chooseone") + if (subElement.Name.ToString().Equals("chooseone", StringComparison.OrdinalIgnoreCase)) { groupIndex++; LoadEntities(subElement, ref groupIndex); @@ -390,7 +390,7 @@ namespace Barotrauma.RuinGeneration SourceEntityIdentifier = element.GetAttributeString("sourceentity", ""); foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() == "wire") + if (subElement.Name.ToString().Equals("wire", StringComparison.OrdinalIgnoreCase)) { WireConnection = new Pair( subElement.GetAttributeString("from", ""), diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs index 339c1423b..422f731bb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/LinkedSubmarine.cs @@ -19,18 +19,17 @@ namespace Barotrauma //Prefabs.Remove(this); } - public readonly Submarine mainSub; + public readonly SubmarineInfo subInfo; - public LinkedSubmarinePrefab(Submarine submarine) + public LinkedSubmarinePrefab(SubmarineInfo subInfo) { - this.mainSub = submarine; + this.subInfo = subInfo; } protected override void CreateInstance(Rectangle rect) { System.Diagnostics.Debug.Assert(Submarine.MainSub != null); - - LinkedSubmarine.CreateDummy(Submarine.MainSub, mainSub.FilePath, rect.Location.ToVector2()); + LinkedSubmarine.CreateDummy(Submarine.MainSub, subInfo.FilePath, rect.Location.ToVector2()); } } @@ -92,7 +91,7 @@ namespace Barotrauma public static LinkedSubmarine CreateDummy(Submarine mainSub, string filePath, Vector2 position) { - XDocument doc = Submarine.OpenFile(filePath); + XDocument doc = SubmarineInfo.OpenFile(filePath); if (doc == null || doc.Root == null) return null; LinkedSubmarine sl = CreateDummy(mainSub, doc.Root, position); @@ -113,7 +112,9 @@ namespace Barotrauma (int)sl.wallVertices.Max(v => v.X + position.X), (int)sl.wallVertices.Min(v => v.Y + position.Y)); - sl.Rect = new Rectangle(sl.rect.X, sl.rect.Y, sl.rect.Width - sl.rect.X, sl.rect.Y - sl.rect.Height); + int width = sl.rect.Width - sl.rect.X; + int height = sl.rect.Y - sl.rect.Height; + sl.Rect = new Rectangle((int)(position.X - width / 2), (int)(position.Y + height / 2), width, height); } else { @@ -162,8 +163,7 @@ namespace Barotrauma public static LinkedSubmarine Load(XElement element, Submarine submarine) { Vector2 pos = element.GetAttributeVector2("pos", Vector2.Zero); - LinkedSubmarine linkedSub = null; - + LinkedSubmarine linkedSub; if (Screen.Selected == GameMain.SubEditorScreen) { linkedSub = CreateDummy(submarine, element, pos); @@ -198,26 +198,32 @@ namespace Barotrauma for (int i = 0; i < linkedToIds.Length; i++) { linkedSub.linkedToID.Add((ushort)linkedToIds[i]); - if (Screen.Selected == GameMain.SubEditorScreen) - { - if (FindEntityByID((ushort)linkedToIds[i]) is MapEntity linked) - { - linkedSub.linkedTo.Add(linked); - } - } } linkedSub.originalLinkedToID = (ushort)element.GetAttributeInt("originallinkedto", 0); linkedSub.originalMyPortID = (ushort)element.GetAttributeInt("originalmyport", 0); - return linkedSub.loadSub ? linkedSub : null; } + public void LinkDummyToMainSubmarine() + { + if (Screen.Selected != GameMain.SubEditorScreen) { return; } + for (int i = 0; i < linkedToID.Count; i++) + { + if (FindEntityByID(linkedToID[i]) is MapEntity linked) + { + linkedTo.Add(linked); + } + } + } + + public override void OnMapLoaded() { if (!loadSub) { return; } - sub = Submarine.Load(saveElement, false); + SubmarineInfo info = new SubmarineInfo(Submarine.Info.FilePath, "", saveElement); + sub = Submarine.Load(info, false); Vector2 worldPos = saveElement.GetAttributeVector2("worldpos", Vector2.Zero); if (worldPos != Vector2.Zero) @@ -330,7 +336,7 @@ namespace Barotrauma { if (this.saveElement == null) { - var doc = Submarine.OpenFile(filePath); + var doc = SubmarineInfo.OpenFile(filePath); saveElement = doc.Root; saveElement.Name = "LinkedSubmarine"; saveElement.Add(new XAttribute("filepath", filePath)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index 1bca7d130..d1b5bff03 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -284,7 +284,7 @@ namespace Barotrauma foreach (LocationConnection connection in connections) { float centerDist = Vector2.Distance(connection.CenterPos, mapCenter); - connection.Difficulty = MathHelper.Clamp(((1.0f - centerDist / mapRadius) * 100) + Rand.Range(-10.0f, 10.0f, Rand.RandSync.Server), 0, 100); + connection.Difficulty = MathHelper.Clamp(((1.0f - centerDist / mapRadius) * 100) + Rand.Range(-10.0f, 0.0f, Rand.RandSync.Server), 0, 100); } AssignBiomes(); @@ -463,7 +463,7 @@ namespace Barotrauma bool disallowedFound = false; foreach (string disallowedLocationName in typeChange.DisallowedAdjacentLocations) { - if (location.Connections.Any(c => c.OtherLocation(location).Type.Identifier.ToLowerInvariant() == disallowedLocationName.ToLowerInvariant())) + if (location.Connections.Any(c => c.OtherLocation(location).Type.Identifier.Equals(disallowedLocationName, StringComparison.OrdinalIgnoreCase))) { disallowedFound = true; break; @@ -475,7 +475,7 @@ namespace Barotrauma bool requiredFound = false; foreach (string requiredLocationName in typeChange.RequiredAdjacentLocations) { - if (location.Connections.Any(c => c.OtherLocation(location).Type.Identifier.ToLowerInvariant() == requiredLocationName.ToLowerInvariant())) + if (location.Connections.Any(c => c.OtherLocation(location).Type.Identifier.Equals(requiredLocationName, StringComparison.OrdinalIgnoreCase))) { requiredFound = true; break; @@ -499,7 +499,7 @@ namespace Barotrauma if (selectedTypeChange != null) { string prevName = location.Name; - location.ChangeType(LocationType.List.Find(lt => lt.Identifier.ToLowerInvariant() == selectedTypeChange.ChangeToType.ToLowerInvariant())); + location.ChangeType(LocationType.List.Find(lt => lt.Identifier.Equals(selectedTypeChange.ChangeToType, StringComparison.OrdinalIgnoreCase))); ChangeLocationType(location, prevName, selectedTypeChange); location.TypeChangeTimer = -1; break; @@ -553,13 +553,12 @@ namespace Barotrauma string prevLocationName = location.Name; LocationType prevLocationType = location.Type; location.Discovered = true; - location.ChangeType(LocationType.List.Find(lt => lt.Identifier.ToLowerInvariant() == locationType.ToLowerInvariant())); + location.ChangeType(LocationType.List.Find(lt => lt.Identifier.Equals(locationType, StringComparison.OrdinalIgnoreCase))); location.TypeChangeTimer = typeChangeTimer; location.MissionsCompleted = missionsCompleted; if (showNotifications && prevLocationType != location.Type) { - var change = prevLocationType.CanChangeTo.Find(c => - c.ChangeToType.ToLowerInvariant() == location.Type.Identifier.ToLowerInvariant()); + var change = prevLocationType.CanChangeTo.Find(c => c.ChangeToType.Equals(location.Type.Identifier, StringComparison.OrdinalIgnoreCase)); if (change != null) { ChangeLocationType(location, prevLocationName, change); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs index 1f03417ba..ff075b815 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntity.cs @@ -36,8 +36,6 @@ namespace Barotrauma //is the mouse inside the rect private bool isHighlighted; - public event Action Resized; - public bool IsHighlighted { get { return isHighlighted || ExternalHighlight; } @@ -115,6 +113,40 @@ namespace Barotrauma } } + // We could use NaN or nullables, but in this case the first is not preferable, because it needs to be checked every time the value is used. + // Nullable on the other requires boxing that we don't want to do too often, since it generates garbage. + public bool SpriteDepthOverrideIsSet { get; private set; } + public float SpriteOverrideDepth => SpriteDepth; + private float _spriteOverrideDepth = float.NaN; + [Editable(0.001f, 0.999f, decimals: 3), Serialize(float.NaN, true)] + public float SpriteDepth + { + get + { + if (SpriteDepthOverrideIsSet) { return _spriteOverrideDepth; } + return Sprite != null ? Sprite.Depth : 0; + } + set + { + if (!float.IsNaN(value)) + { + _spriteOverrideDepth = MathHelper.Clamp(value, 0.001f, 0.999f); + if (this is Item) { _spriteOverrideDepth = Math.Min(_spriteOverrideDepth, 0.9f); } + SpriteDepthOverrideIsSet = true; + } + } + } + + [Serialize(1f, true), Editable(0.01f, 10f, DecimalCount = 3, ValueStep = 0.1f)] + public virtual float Scale { get; set; } = 1; + + [Editable, Serialize(false, true)] + public bool HiddenInGame + { + get; + set; + } + public override Vector2 Position { get @@ -175,9 +207,6 @@ namespace Barotrauma get { return ""; } } - // Quick undo/redo for size and movement only. TODO: Remove if we do a more general implementation. - private Memento rectMemento; - public MapEntity(MapEntityPrefab prefab, Submarine submarine) : base(submarine) { this.prefab = prefab; @@ -560,34 +589,5 @@ namespace Barotrauma } } } - - #region Serialized properties - // We could use NaN or nullables, but in this case the first is not preferable, because it needs to be checked every time the value is used. - // Nullable on the other requires boxing that we don't want to do too often, since it generates garbage. - public bool SpriteDepthOverrideIsSet { get; private set; } - public float SpriteOverrideDepth => SpriteDepth; - private float _spriteOverrideDepth = float.NaN; - [Editable(0.001f, 0.999f, decimals: 3), Serialize(float.NaN, true)] - public float SpriteDepth - { - get - { - if (SpriteDepthOverrideIsSet) { return _spriteOverrideDepth; } - return Sprite != null ? Sprite.Depth : 0; - } - set - { - if (!float.IsNaN(value)) - { - _spriteOverrideDepth = MathHelper.Clamp(value, 0.001f, 0.999f); - if (this is Item) { _spriteOverrideDepth = Math.Min(_spriteOverrideDepth, 0.9f); } - SpriteDepthOverrideIsSet = true; - } - } - } - - [Serialize(1f, true), Editable(0.01f, 10f, DecimalCount = 3, ValueStep = 0.1f)] - public virtual float Scale { get; set; } = 1; - #endregion } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs index b2ff9d02f..31b6bc9cc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/MapEntityPrefab.cs @@ -10,7 +10,7 @@ namespace Barotrauma [Flags] enum MapEntityCategory { - Structure = 1, Decorative = 2, Machine = 4, Equipment = 8, Electrical = 16, Material = 32, Misc = 64, Alien = 128, ItemAssembly = 256, Legacy = 512 + Structure = 1, Decorative = 2, Machine = 4, Equipment = 8, Electrical = 16, Material = 32, Misc = 64, Alien = 128, Wrecked = 256, Thalamus = 512, ItemAssembly = 1024, Legacy = 2048 } abstract partial class MapEntityPrefab : IPrefab, IDisposable @@ -242,23 +242,35 @@ namespace Barotrauma /// The identifier of the item (if null, the identifier is ignored and the search is done only based on the name) public static MapEntityPrefab Find(string name, string identifier = null, bool showErrorMessages = true) { - if (name != null) name = name.ToLowerInvariant(); + if (name != null) + { + name = name.ToLowerInvariant(); + } foreach (MapEntityPrefab prefab in List) { if (identifier != null) { if (prefab.identifier != identifier) { + if (prefab.Aliases != null && prefab.Aliases.Any(a => a.Equals(identifier, StringComparison.OrdinalIgnoreCase))) + { + return prefab; + } continue; } else { - if (string.IsNullOrEmpty(name)) return prefab; + if (string.IsNullOrEmpty(name)) { return prefab; } } } if (!string.IsNullOrEmpty(name)) { - if (prefab.Name.ToLowerInvariant() == name || prefab.originalName.ToLowerInvariant() == name || (prefab.Aliases != null && prefab.Aliases.Any(a => a.ToLowerInvariant() == name))) return prefab; + if (prefab.Name.Equals(name, StringComparison.OrdinalIgnoreCase) || + prefab.originalName.Equals(name, StringComparison.OrdinalIgnoreCase) || + (prefab.Aliases != null && prefab.Aliases.Any(a => a.Equals(name, StringComparison.OrdinalIgnoreCase)))) + { + return prefab; + } } } @@ -281,27 +293,9 @@ namespace Barotrauma /// /// Check if the name or any of the aliases of this prefab match the given name. /// - public bool NameMatches(string name, bool caseSensitive = false) - { - if (caseSensitive) - { - return this.originalName == name || (Aliases != null && Aliases.Any(a => a == name)); - } - else - { - name = name.ToLowerInvariant(); - return this.originalName.ToLowerInvariant() == name || (Aliases != null && Aliases.Any(a => a.ToLowerInvariant() == name)); - } - } + public bool NameMatches(string name, StringComparison comparisonType) => originalName.Equals(name, comparisonType) || (Aliases != null && Aliases.Any(a => a.Equals(name, comparisonType))); - public bool NameMatches(IEnumerable allowedNames, bool caseSensitive = false) - { - foreach (string name in allowedNames) - { - if (NameMatches(name, caseSensitive)) return true; - } - return false; - } + public bool NameMatches(IEnumerable allowedNames, StringComparison comparisonType) => allowedNames.Any(n => NameMatches(n, comparisonType)); public bool IsLinkAllowed(MapEntityPrefab target) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/RoundEndCinematic.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/RoundEndCinematic.cs index 6ea887a58..0964e41bc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/RoundEndCinematic.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/RoundEndCinematic.cs @@ -56,6 +56,15 @@ namespace Barotrauma Level.Loaded.TopBarrier.Enabled = false; + foreach (Character character in Character.CharacterList) + { + character.AnimController.Frozen = true; + foreach (Limb limb in character.AnimController.Limbs) + { + limb.body.PhysEnabled = false; + } + } + cam.TargetPos = Vector2.Zero; float timer = 0.0f; float initialZoom = cam.Zoom; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs index a3d64c723..a6cf23e7e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Structure.cs @@ -322,7 +322,7 @@ namespace Barotrauma : base(sp, submarine) { System.Diagnostics.Debug.Assert(rectangle.Width > 0 && rectangle.Height > 0); - if (rectangle.Width == 0 || rectangle.Height == 0) return; + if (rectangle.Width == 0 || rectangle.Height == 0) { return; } defaultRect = rectangle; rect = rectangle; @@ -358,27 +358,30 @@ namespace Barotrauma InitProjSpecific(); - if (Prefab.Body) + if (!HiddenInGame) { - Bodies = new List(); - WallList.Add(this); - - CreateSections(); - UpdateSections(); - } - else - { - Sections = new WallSection[1]; - Sections[0] = new WallSection(rect); - - if (StairDirection != Direction.None) + if (Prefab.Body) { - CreateStairBodies(); + Bodies = new List(); + WallList.Add(this); + + CreateSections(); + UpdateSections(); + } + else + { + Sections = new WallSection[1]; + Sections[0] = new WallSection(rect); + + if (StairDirection != Direction.None) + { + CreateStairBodies(); + } } } // Only add ai targets automatically to submarine/outpost walls - if (aiTarget == null && HasBody && Tags.Contains("wall") && submarine != null && !Prefab.NoAITarget) + if (aiTarget == null && HasBody && Tags.Contains("wall") && submarine != null && !submarine.Info.IsWreck && !Prefab.NoAITarget) { aiTarget = new AITarget(this) { @@ -910,7 +913,7 @@ namespace Barotrauma //the structure doesn't have any other gap, log the structure being fixed if (noGaps && attacker != null) { - GameServer.Log((Sections[sectionIndex].gap.IsRoomToRoom ? "Inner" : "Outer") + " wall repaired by " + attacker.Name, ServerLog.MessageType.ItemInteraction); + GameServer.Log((Sections[sectionIndex].gap.IsRoomToRoom ? "Inner" : "Outer") + " wall repaired by " + GameServer.CharacterLogName(attacker), ServerLog.MessageType.ItemInteraction); } #endif DebugConsole.Log("Removing gap (ID " + Sections[sectionIndex].gap.ID + ", section: " + sectionIndex + ") from wall " + ID); @@ -984,11 +987,11 @@ namespace Barotrauma //the structure didn't have any other gaps yet, log the breach if (noGaps && attacker != null) { - GameServer.Log((Sections[sectionIndex].gap.IsRoomToRoom ? "Inner" : "Outer") + " wall breached by " + attacker.Name, ServerLog.MessageType.ItemInteraction); + GameServer.Log((Sections[sectionIndex].gap.IsRoomToRoom ? "Inner" : "Outer") + " wall breached by " + GameServer.CharacterLogName(attacker), ServerLog.MessageType.ItemInteraction); } #endif } - + float gapOpen = (damage / Prefab.Health - LeakThreshold) * (1.0f / (1.0f - LeakThreshold)); Sections[sectionIndex].gap.Open = gapOpen; } @@ -1216,9 +1219,9 @@ namespace Barotrauma SerializableProperty.DeserializeProperties(s, element); - if (submarine?.GameVersion != null) + if (submarine?.Info.GameVersion != null) { - SerializableProperty.UpgradeGameVersion(s, s.Prefab.ConfigElement, submarine.GameVersion); + SerializableProperty.UpgradeGameVersion(s, s.Prefab.ConfigElement, submarine.Info.GameVersion); } foreach (XElement subElement in element.Elements()) @@ -1322,6 +1325,8 @@ namespace Barotrauma public virtual void Reset() { SerializableProperties = SerializableProperty.DeserializeProperties(this, Prefab.ConfigElement); + Sprite.ReloadXML(); + SpriteDepth = Sprite.Depth; } public override void Update(float deltaTime, Camera cam) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs index ef8076b81..51df613ad 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/StructurePrefab.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using System.Collections.Generic; using System.Xml.Linq; +using System.IO; #if CLIENT using Microsoft.Xna.Framework.Graphics; #endif @@ -231,6 +232,23 @@ namespace Barotrauma sp.name = sp.originalName; sp.ConfigElement = element; sp.identifier = element.GetAttributeString("identifier", ""); + + var parentType = element.Parent?.GetAttributeString("prefabtype", "") ?? string.Empty; + + string nameIdentifier = element.GetAttributeString("nameidentifier", ""); + + if (string.IsNullOrEmpty(sp.originalName)) + { + if (string.IsNullOrEmpty(nameIdentifier)) + { + sp.name = TextManager.Get("EntityName." + sp.identifier, true) ?? string.Empty; + } + else + { + sp.name = TextManager.Get("EntityName." + nameIdentifier, true) ?? string.Empty; + } + } + if (string.IsNullOrEmpty(sp.name)) { sp.name = TextManager.Get("EntityName." + sp.identifier, returnNull: true) ?? $"Not defined ({sp.identifier})"; @@ -258,17 +276,15 @@ namespace Barotrauma { DebugConsole.ThrowError("Warning - sprite sourcerect not configured for structure \"" + sp.name + "\"!"); } - #if CLIENT - if (subElement.GetAttributeBool("fliphorizontal", false)) + if (subElement.GetAttributeBool("fliphorizontal", false)) sp.sprite.effects = SpriteEffects.FlipHorizontally; - if (subElement.GetAttributeBool("flipvertical", false)) + if (subElement.GetAttributeBool("flipvertical", false)) sp.sprite.effects = SpriteEffects.FlipVertically; #endif - sp.canSpriteFlipX = subElement.GetAttributeBool("canflipx", true); sp.canSpriteFlipY = subElement.GetAttributeBool("canflipy", true); - + if (subElement.Attribute("name") == null && !string.IsNullOrWhiteSpace(sp.Name)) { sp.sprite.Name = sp.Name; @@ -286,19 +302,52 @@ namespace Barotrauma sp.BackgroundSprite.RelativeOrigin = subElement.GetAttributeVector2("origin", new Vector2(0.5f, 0.5f)); } #if CLIENT - if (subElement.GetAttributeBool("fliphorizontal", false)) - sp.BackgroundSprite.effects = SpriteEffects.FlipHorizontally; - if (subElement.GetAttributeBool("flipvertical", false)) - sp.BackgroundSprite.effects = SpriteEffects.FlipVertically; + if (subElement.GetAttributeBool("fliphorizontal", false)) { sp.BackgroundSprite.effects = SpriteEffects.FlipHorizontally; } + if (subElement.GetAttributeBool("flipvertical", false)) { sp.BackgroundSprite.effects = SpriteEffects.FlipVertically; } + sp.BackgroundSpriteColor = subElement.GetAttributeColor("color", Color.White); #endif - break; + case "decorativesprite": +#if CLIENT + string decorativeSpriteFolder = ""; + if (!subElement.GetAttributeString("texture", "").Contains("/")) + { + decorativeSpriteFolder = Path.GetDirectoryName(file.Path); + } + + int groupID = 0; + DecorativeSprite decorativeSprite = null; + if (subElement.Attribute("texture") == null) + { + groupID = subElement.GetAttributeInt("randomgroupid", 0); + } + else + { + decorativeSprite = new DecorativeSprite(subElement, decorativeSpriteFolder, lazyLoad: true); + sp.DecorativeSprites.Add(decorativeSprite); + groupID = decorativeSprite.RandomGroupID; + } + if (!sp.DecorativeSpriteGroups.ContainsKey(groupID)) + { + sp.DecorativeSpriteGroups.Add(groupID, new List()); + } + sp.DecorativeSpriteGroups[groupID].Add(decorativeSprite); +#endif + break; + } + } + + if (string.Equals(parentType, "wrecked", StringComparison.OrdinalIgnoreCase)) + { + if (!string.IsNullOrEmpty(sp.Name)) + { + sp.name = TextManager.GetWithVariable("wreckeditemformat", "[name]", sp.name); } } if (!Enum.TryParse(element.GetAttributeString("category", "Structure"), true, out MapEntityCategory category)) { - category = MapEntityCategory.Structure; + category = MapEntityCategory.Structure; } sp.Category = category; @@ -325,7 +374,14 @@ namespace Barotrauma if (string.IsNullOrEmpty(sp.Description)) { - sp.Description = TextManager.Get("EntityDescription." + sp.identifier, returnNull: true) ?? string.Empty; + if (string.IsNullOrEmpty(nameIdentifier)) + { + sp.Description = TextManager.Get("EntityDescription." + sp.identifier, returnNull: true) ?? string.Empty; + } + else + { + sp.Description = TextManager.Get("EntityDescription." + nameIdentifier, true) ?? string.Empty; + } } //backwards compatibility diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs index c04680ee5..583311994 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs @@ -1,6 +1,6 @@ using Barotrauma.Items.Components; using Barotrauma.Networking; -using Barotrauma.RuinGeneration; +using Barotrauma.Extensions; using FarseerPhysics; using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; @@ -21,20 +21,11 @@ namespace Barotrauma None = 0, Left = 1, Right = 2 } - [Flags] - public enum SubmarineTag - { - [Description("Shuttle")] - Shuttle = 1, - [Description("Hide in menus")] - HideInMenus = 2 - } - partial class Submarine : Entity, IServerSerializable { - public Character.TeamType TeamID = Character.TeamType.None; + public SubmarineInfo Info { get; private set; } - public const string SavePath = "Submarines"; + public Character.TeamType TeamID = Character.TeamType.None; public static readonly Vector2 HiddenSubStartPosition = new Vector2(-50000.0f, 10000.0f); //position of the "actual submarine" which is rendered wherever the SubmarineBody is @@ -53,12 +44,6 @@ namespace Barotrauma public static bool LockX, LockY; - private static List savedSubmarines = new List(); - public static IEnumerable SavedSubmarines - { - get { return savedSubmarines; } - } - public static readonly Vector2 GridSize = new Vector2(16.0f, 16.0f); public static readonly Submarine[] MainSubs = new Submarine[2]; @@ -94,60 +79,16 @@ namespace Barotrauma private static float lastPickedFraction; private static Vector2 lastPickedNormal; - private Task hashTask; - private Md5Hash hash; - - private string filePath; - private string name; - public readonly DateTime LastModifiedTime; - - private SubmarineTag tags; - private Vector2 prevPosition; private float networkUpdateTimer; private EntityGrid entityGrid = null; - public int RecommendedCrewSizeMin = 1, RecommendedCrewSizeMax = 2; - public string RecommendedCrewExperience; - - public HashSet RequiredContentPackages = new HashSet(); - //properties ---------------------------------------------------- - public string Name - { - get { return name; } - set { name = value; } - } - - private string displayName; - public string DisplayName - { - get { return displayName; } - } - public bool ShowSonarMarker = true; - public string Description - { - get; - set; - } - - public Version GameVersion - { - get; - private set; - } - - public bool IsOutpost - { - get; - private set; - } - public static Vector2 LastPickedPosition { get { return lastPickedPosition; } @@ -175,22 +116,6 @@ namespace Barotrauma set; } - public Md5Hash MD5Hash - { - get - { - if (hash == null) - { - XDocument doc = OpenFile(filePath); - StartHashDocTask(doc); - hashTask.Wait(); - hashTask = null; - } - - return hash; - } - } - public static List Loaded { get { return loaded; } @@ -203,7 +128,7 @@ namespace Barotrauma public PhysicsBody PhysicsBody { - get { return subBody.Body; } + get { return subBody?.Body; } } public Rectangle Borders @@ -214,12 +139,6 @@ namespace Barotrauma } } - public Vector2 Dimensions - { - get; - private set; - } - public override Vector2 Position { get { return subBody == null ? Vector2.Zero : subBody.Position - HiddenSubPosition; } @@ -259,22 +178,6 @@ namespace Barotrauma } } - private bool? subsLeftBehind; - public bool SubsLeftBehind - { - get - { - if (subsLeftBehind.HasValue) { return subsLeftBehind.Value; } - - CheckSubsLeftBehind(); - return subsLeftBehind.Value; - } - //set { subsLeftBehind = value; } - } - public bool LeftBehindSubDockingPortOccupied - { - get; private set; - } public new Vector2 DrawPosition { @@ -302,14 +205,7 @@ namespace Barotrauma public List HullVertices { - get { return subBody.HullVertices; } - } - - - public string FilePath - { - get { return filePath; } - set { filePath = value; } + get { return subBody?.HullVertices; } } public bool AtDamageDepth @@ -319,7 +215,7 @@ namespace Barotrauma public override string ToString() { - return "Barotrauma.Submarine (" + name + ")"; + return "Barotrauma.Submarine (" + Info?.Name ?? "[NULL INFO]" + ")"; } public override bool Removed @@ -330,256 +226,88 @@ namespace Barotrauma } } - public bool IsFileCorrupted + public void MakeWreck() { - get; - private set; - } - - private bool? requiredContentPackagesInstalled; - public bool RequiredContentPackagesInstalled - { - get - { - if (requiredContentPackagesInstalled.HasValue) { return requiredContentPackagesInstalled.Value; } - return RequiredContentPackages.All(cp => GameMain.SelectedPackages.Any(cp2 => cp2.Name == cp)); - } - set - { - requiredContentPackagesInstalled = value; - } - } - - //constructors & generation ---------------------------------------------------- - - public Submarine(string filePath, string hash = "", bool tryLoad = true) : base(null) - { - this.filePath = filePath; - if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath)) - { - LastModifiedTime = File.GetLastWriteTime(filePath); - } - try - { - name = displayName = Path.GetFileNameWithoutExtension(filePath); - } - catch (Exception e) - { - DebugConsole.ThrowError("Error loading submarine " + filePath + "!", e); - } - - if (!string.IsNullOrWhiteSpace(hash)) - { - this.hash = new Md5Hash(hash); - } - - IsFileCorrupted = false; - - if (tryLoad) - { - XDocument doc = null; - int maxLoadRetries = 4; - for (int i = 0; i <= maxLoadRetries; i++) - { - doc = OpenFile(filePath, out Exception e); - if (e != null && !(e is IOException)) { break; } - if (doc != null || i == maxLoadRetries || !File.Exists(filePath)) { break; } - DebugConsole.NewMessage("Opening submarine file \"" + filePath + "\" failed, retrying in 250 ms..."); - Thread.Sleep(250); - } - if (doc == null || doc.Root == null) - { - IsFileCorrupted = true; - return; - } - - if (doc != null && doc.Root != null) - { - if (string.IsNullOrWhiteSpace(hash)) - { - StartHashDocTask(doc); - } - - displayName = TextManager.Get("Submarine.Name." + name, true); - if (displayName == null || displayName.Length == 0) displayName = name; - - Description = TextManager.Get("Submarine.Description." + name, true); - if (Description == null || Description.Length == 0) Description = doc.Root.GetAttributeString("description", ""); - - GameVersion = new Version(doc.Root.GetAttributeString("gameversion", "0.0.0.0")); - Enum.TryParse(doc.Root.GetAttributeString("tags", ""), out tags); - Dimensions = doc.Root.GetAttributeVector2("dimensions", Vector2.Zero); - RecommendedCrewSizeMin = doc.Root.GetAttributeInt("recommendedcrewsizemin", 0); - RecommendedCrewSizeMax = doc.Root.GetAttributeInt("recommendedcrewsizemax", 0); - RecommendedCrewExperience = doc.Root.GetAttributeString("recommendedcrewexperience", "Unknown"); - - //backwards compatibility (use text tags instead of the actual text) - if (RecommendedCrewExperience == "Beginner") - RecommendedCrewExperience = "CrewExperienceLow"; - else if (RecommendedCrewExperience == "Intermediate") - RecommendedCrewExperience = "CrewExperienceMid"; - else if (RecommendedCrewExperience == "Experienced") - RecommendedCrewExperience = "CrewExperienceHigh"; - - string[] contentPackageNames = doc.Root.GetAttributeStringArray("requiredcontentpackages", new string[0]); - foreach (string contentPackageName in contentPackageNames) - { - RequiredContentPackages.Add(contentPackageName); - } - - CheckSubsLeftBehind(doc.Root); -#if CLIENT - string previewImageData = doc.Root.GetAttributeString("previewimage", ""); - if (!string.IsNullOrEmpty(previewImageData)) - { - try - { - using (MemoryStream mem = new MemoryStream(Convert.FromBase64String(previewImageData))) - { - var texture = TextureLoader.FromStream(mem, path: filePath); - if (texture == null) { throw new Exception("PreviewImage texture returned null"); } - PreviewImage = new Sprite(texture, null, null); - } - } - catch (Exception e) - { - DebugConsole.ThrowError("Loading the preview image of the submarine \"" + Name + "\" failed. The file may be corrupted.", e); - GameAnalyticsManager.AddErrorEventOnce("Submarine..ctor:PreviewImageLoadingFailed", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, - "Loading the preview image of the submarine \"" + Name + "\" failed. The file may be corrupted."); - PreviewImage = null; - } - } -#endif - } - } - - ConnectedDockingPorts = new Dictionary(); - - FreeID(); - } - - public void StartHashDocTask(XDocument doc) - { - if (hash != null) { return; } - if (hashTask != null) { return; } - - hashTask = new Task(() => - { - hash = new Md5Hash(doc, filePath); - }); - hashTask.Start(); - } - - public bool HasTag(SubmarineTag tag) - { - return tags.HasFlag(tag); - } - - public void AddTag(SubmarineTag tag) - { - if (tags.HasFlag(tag)) return; - - tags |= tag; - } - - public void RemoveTag(SubmarineTag tag) - { - if (!tags.HasFlag(tag)) return; - - tags &= ~tag; - } - - public void CheckSubsLeftBehind(XElement element = null) - { - if (element == null) - { - XDocument doc = null; - int maxLoadRetries = 4; - for (int i = 0; i <= maxLoadRetries; i++) - { - doc = OpenFile(filePath, out Exception e); - if (e != null && !(e is IOException)) { break; } - if (doc != null || i == maxLoadRetries || !File.Exists(filePath)) { break; } - DebugConsole.NewMessage("Opening submarine file \"" + filePath + "\" failed, retrying in 250 ms..."); - Thread.Sleep(250); - } - if (doc?.Root == null) { return; } - element = doc.Root; - } - - subsLeftBehind = false; - LeftBehindSubDockingPortOccupied = false; - foreach (XElement subElement in element.Elements()) - { - if (subElement.Name.ToString().ToLowerInvariant() != "linkedsubmarine") { continue; } - if (subElement.Attribute("location") == null) { continue; } - - subsLeftBehind = true; - ushort targetDockingPortID = (ushort)subElement.GetAttributeInt("originallinkedto", 0); - XElement targetPortElement = targetDockingPortID == 0 ? null : - element.Elements().FirstOrDefault(e => e.GetAttributeInt("ID", 0) == targetDockingPortID); - if (targetPortElement != null && targetPortElement.GetAttributeIntArray("linked", new int[0]).Length > 0) - { - LeftBehindSubDockingPortOccupied = true; - } - } - } - - public void MakeOutpost() - { - IsOutpost = true; + Info.Type = SubmarineInfo.SubmarineType.Wreck; ShowSonarMarker = false; PhysicsBody.FarseerBody.BodyType = BodyType.Static; - TeamID = Character.TeamType.FriendlyNPC; + TeamID = Character.TeamType.None; - foreach (MapEntity me in MapEntity.mapEntityList) + string defaultTag = Level.Loaded.GetWreckIDTag("wreck_id", this); + ReplaceIDCardTagRequirements("wreck_id", defaultTag); + + foreach (Item item in Item.ItemList) { - if (me.Submarine != this) { continue; } - if (me is Item item) + if (item.Submarine != this) { continue; } + if (item.prefab.Identifier == "idcardwreck" || item.prefab.Identifier == "idcard") { - if (item.GetComponent() != null) + foreach (string tag in item.GetTags().ToList()) { - item.Indestructible = true; - } + if (tag == "smallitem") { continue; } + string newTag = Level.Loaded.GetWreckIDTag(tag, this); + item.ReplaceTag(tag, newTag); + ReplaceIDCardTagRequirements(tag, newTag); + } + } + } + + void ReplaceIDCardTagRequirements(string oldTag, string newTag) + { + foreach (Item item in Item.ItemList) + { + if (item.Submarine != this) { continue; } foreach (ItemComponent ic in item.Components) { - if (ic is ConnectionPanel connectionPanel) - { - //prevent rewiring - connectionPanel.Locked = true; - } - else if (ic is Holdable holdable && holdable.Attached) - { - //prevent deattaching items from walls -#if CLIENT - if (GameMain.GameSession?.GameMode is TutorialMode) - { - continue; - } -#endif - holdable.CanBePicked = false; - holdable.CanBeSelected = false; - } + ReplaceIDCardTagRequirement(ic, RelatedItem.RelationType.Picked, oldTag, newTag); + ReplaceIDCardTagRequirement(ic, RelatedItem.RelationType.Equipped, oldTag, newTag); } } - else if (me is Structure structure) + } + + static void ReplaceIDCardTagRequirement(ItemComponent ic, RelatedItem.RelationType relationType, string oldTag, string newTag) + { + if (!ic.requiredItems.ContainsKey(relationType)) { return; } + foreach (RelatedItem requiredItem in ic.requiredItems[relationType]) { - structure.Indestructible = true; + int index = Array.IndexOf(requiredItem.Identifiers, oldTag); + if (index == -1) { continue; } + requiredItem.Identifiers[index] = newTag; } } } + public WreckAI WreckAI { get; private set; } + public bool CreateWreckAI() + { + MakeWreck(); + WreckAI = new WreckAI(this); + return WreckAI != null; + } + + public void DisableWreckAI() + { + if (WreckAI == null) + { + WreckAI.RemoveThalamusItems(this); + } + else + { + WreckAI?.Remove(); + WreckAI = null; + } + } + /// /// Returns a rect that contains the borders of this sub and all subs docked to it /// - public Rectangle GetDockedBorders(List checkd=null) + public Rectangle GetDockedBorders(List checkd = null) { if (checkd == null) { checkd = new List(); } checkd.Add(this); Rectangle dockedBorders = Borders; - var connectedSubs = DockedTo.Where(s => !checkd.Contains(s) && !s.IsOutpost).ToList(); + var connectedSubs = DockedTo.Where(s => !checkd.Contains(s) && !s.Info.IsOutpost).ToList(); foreach (Submarine dockedSub in connectedSubs) { @@ -628,6 +356,9 @@ namespace Barotrauma } } + /// + /// Attempt to find a spawn position close to the specified position where the sub doesn't collide with walls/ruins + /// public Vector2 FindSpawnPos(Vector2 spawnPos, Point? submarineSize = null, float subDockingPortOffset = 0.0f) { Rectangle dockedBorders = GetDockedBorders(); @@ -693,9 +424,11 @@ namespace Barotrauma return spawnPos - diffFromDockedBorders; } - public void UpdateTransform() + public void UpdateTransform(bool interpolate = true) { - DrawPosition = Timing.Interpolate(prevPosition, Position); + DrawPosition = interpolate ? + Timing.Interpolate(prevPosition, Position) : + Position; } //math/physics stuff ---------------------------------------------------- @@ -786,9 +519,9 @@ namespace Barotrauma public static Body PickBody(Vector2 rayStart, Vector2 rayEnd, IEnumerable ignoredBodies = null, Category? collisionCategory = null, bool ignoreSensors = true, Predicate customPredicate = null, bool allowInsideFixture = false) { - if (Vector2.DistanceSquared(rayStart, rayEnd) < 0.00001f) + if (Vector2.DistanceSquared(rayStart, rayEnd) < 0.0001f) { - rayEnd += Vector2.UnitX * 0.001f; + return null; } float closestFraction = 1.0f; @@ -1085,7 +818,12 @@ namespace Barotrauma { //if (PlayerInput.KeyHit(InputType.Crouch) && (this == MainSub)) FlipX(); - if (Level.Loaded == null || subBody == null) return; + if (Level.Loaded == null || subBody == null) { return; } + + if (Info.IsWreck) + { + WreckAI?.Update(deltaTime); + } if (WorldPosition.Y < Level.MaxEntityDepth && subBody.Body.Enabled && @@ -1134,7 +872,6 @@ namespace Barotrauma { networkUpdateTimer = 1.0f; } - } public void ApplyForce(Vector2 force) @@ -1157,16 +894,23 @@ namespace Barotrauma checkd.Add(this); subBody.SetPosition(position); + UpdateTransform(interpolate: false); foreach (Submarine dockedSub in DockedTo) { + if (dockedSub.Info.IsOutpost) + { + if (ConnectedDockingPorts.TryGetValue(dockedSub, out DockingPort port)) + { + port.Undock(); + continue; + } + } Vector2? expectedLocation = CalculateDockOffset(this, dockedSub); if (expectedLocation == null) { continue; } - dockedSub.SetPosition(position + expectedLocation.Value, checkd); + dockedSub.UpdateTransform(interpolate: false); } - //Level.Loaded.SetPosition(-position); - //prevPosition = position; } public static Vector2? CalculateDockOffset(Submarine sub, Submarine dockedSub) @@ -1183,8 +927,6 @@ namespace Barotrauma if (amount == Vector2.Zero || !MathUtils.IsValid(amount)) return; subBody.SetPosition(subBody.Position + amount); - - //Level.Loaded.Move(-amount); } public static Submarine FindClosest(Vector2 worldPosition, bool ignoreOutposts = false, bool ignoreOutsideLevel = true) @@ -1193,7 +935,7 @@ namespace Barotrauma float closestDist = 0.0f; foreach (Submarine sub in loaded) { - if (ignoreOutposts && sub.IsOutpost) { continue; } + if (ignoreOutposts && sub.Info.IsOutpost) { continue; } if (ignoreOutsideLevel && Level.Loaded != null && sub.WorldPosition.Y > Level.Loaded.Size.Y) { continue; } float dist = Vector2.DistanceSquared(worldPosition, sub.WorldPosition); if (closest == null || dist < closestDist) @@ -1214,12 +956,19 @@ namespace Barotrauma public List GetHulls(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Hull.hullList); public List GetGaps(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Gap.GapList); public List GetItems(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Item.ItemList); + public List GetWaypoints(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, WayPoint.WayPointList); + public List GetWalls(bool alsoFromConnectedSubs) => GetEntities(alsoFromConnectedSubs, Structure.WallList); public List GetEntities(bool includingConnectedSubs, List list) where T : MapEntity { return list.FindAll(e => IsEntityFoundOnThisSub(e, includingConnectedSubs)); } + public IEnumerable GetEntities(bool includingConnectedSubs, IEnumerable list) where T : MapEntity + { + return list.Where(e => IsEntityFoundOnThisSub(e, includingConnectedSubs)); + } + public bool IsEntityFoundOnThisSub(MapEntity entity, bool includingConnectedSubs) { if (entity == null) { return false; } @@ -1227,7 +976,7 @@ namespace Barotrauma if (entity.Submarine == null) { return false; } if (includingConnectedSubs) { - return GetConnectedSubs().Any(s => s == entity.Submarine && entity.Submarine.TeamID == TeamID); + return GetConnectedSubs().Any(s => s == entity.Submarine && entity.Submarine.TeamID == TeamID && entity.Submarine.Info.Type == Info.Type); } return false; } @@ -1237,7 +986,7 @@ namespace Barotrauma /// public static Submarine FindContaining(Vector2 position) { - foreach (Submarine sub in Submarine.Loaded) + foreach (Submarine sub in Loaded) { Rectangle subBorders = sub.Borders; subBorders.Location += MathUtils.ToPoint(sub.HiddenSubPosition) - new Microsoft.Xna.Framework.Point(0, sub.Borders.Height); @@ -1249,255 +998,37 @@ namespace Barotrauma return null; } - - //saving/loading ---------------------------------------------------- - - public static void AddToSavedSubs(Submarine sub) + public static Rectangle GetBorders(XElement submarineElement) { - savedSubmarines.Add(sub); + Vector4 bounds = Vector4.Zero; + foreach (XElement element in submarineElement.Elements()) + { + if (element.Name != "Structure") { continue; } + + string name = element.GetAttributeString("name", ""); + string identifier = element.GetAttributeString("identifier", ""); + StructurePrefab prefab = Structure.FindPrefab(name, identifier); + if (prefab == null || !prefab.Body) { continue; } + + var rect = element.GetAttributeRect("rect", Rectangle.Empty); + bounds = new Vector4( + Math.Min(rect.X, bounds.X), + Math.Max(rect.Y, bounds.Y), + Math.Max(rect.Right, bounds.Z), + Math.Min(rect.Y - rect.Height, bounds.W)); + } + + return new Rectangle((int)bounds.X, (int)bounds.Y, (int)(bounds.Z - bounds.X), (int)(bounds.Y - bounds.W)); } - public static void RefreshSavedSub(string filePath) + public Submarine(SubmarineInfo info, bool showWarningMessages = true) : base(null) { - string fullPath = Path.GetFullPath(filePath); - for (int i = savedSubmarines.Count - 1; i >= 0; i--) - { - if (Path.GetFullPath(savedSubmarines[i].filePath) == fullPath) - { - savedSubmarines[i].Dispose(); - } - } - if (File.Exists(filePath)) - { - var sub = new Submarine(filePath); - if (!sub.IsFileCorrupted) - { - savedSubmarines.Add(sub); - } - savedSubmarines = savedSubmarines.OrderBy(s => s.filePath ?? "").ToList(); - } - } - - public static void RefreshSavedSubs() - { - var contentPackageSubs = ContentPackage.GetFilesOfType(GameMain.Config.SelectedContentPackages, ContentType.Submarine); - - for (int i = savedSubmarines.Count - 1; i>= 0; i--) - { - if (File.Exists(savedSubmarines[i].FilePath) && - savedSubmarines[i].LastModifiedTime == File.GetLastWriteTime(savedSubmarines[i].FilePath) && - (Path.GetFullPath(Path.GetDirectoryName(savedSubmarines[i].FilePath)) == Path.GetFullPath(SavePath) || - contentPackageSubs.Any(fp => Path.GetFullPath(fp.Path).CleanUpPath() == Path.GetFullPath(savedSubmarines[i].FilePath).CleanUpPath()))) - { - continue; - } - savedSubmarines[i].Dispose(); - } - - if (!Directory.Exists(SavePath)) - { - try - { - Directory.CreateDirectory(SavePath); - } - catch (Exception e) - { - DebugConsole.ThrowError("Directory \"" + SavePath + "\" not found and creating the directory failed.", e); - return; - } - } - - List filePaths; - string[] subDirectories; - - try - { - filePaths = Directory.GetFiles(SavePath).ToList(); - subDirectories = Directory.GetDirectories(SavePath); - } - catch (Exception e) - { - DebugConsole.ThrowError("Couldn't open directory \"" + SavePath + "\"!", e); - return; - } - - foreach (string subDirectory in subDirectories) - { - try - { - filePaths.AddRange(Directory.GetFiles(subDirectory).ToList()); - } - catch (Exception e) - { - DebugConsole.ThrowError("Couldn't open subdirectory \"" + subDirectory + "\"!", e); - return; - } - } - - foreach (ContentFile subFile in contentPackageSubs) - { - if (!filePaths.Any(fp => Path.GetFullPath(fp) == Path.GetFullPath(subFile.Path))) - { - filePaths.Add(subFile.Path); - } - } - - filePaths.RemoveAll(p => savedSubmarines.Any(sub => sub.FilePath == p)); - - foreach (string path in filePaths) - { - var sub = new Submarine(path); - if (sub.IsFileCorrupted) - { -#if CLIENT - if (DebugConsole.IsOpen) { DebugConsole.Toggle(); } - var deleteSubPrompt = new GUIMessageBox( - TextManager.Get("Error"), - TextManager.GetWithVariable("SubLoadError", "[subname]", sub.name) +"\n"+ - TextManager.GetWithVariable("DeleteFileVerification", "[filename]", sub.name), - new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); - - string filePath = path; - deleteSubPrompt.Buttons[0].OnClicked += (btn, userdata) => - { - try - { - File.Delete(filePath); - } - catch (Exception e) - { - DebugConsole.ThrowError($"Failed to delete file \"{filePath}\".", e); - } - deleteSubPrompt.Close(); - return true; - }; - deleteSubPrompt.Buttons[1].OnClicked += deleteSubPrompt.Close; -#endif - } - else - { - savedSubmarines.Add(sub); - } - } - } - - static readonly string TempFolder = Path.Combine("Submarine", "Temp"); - - public static XDocument OpenFile(string file) - { - return OpenFile(file, out _); - } - - public static XDocument OpenFile(string file, out Exception exception) - { - XDocument doc = null; - string extension = ""; - exception = null; - - try - { - extension = System.IO.Path.GetExtension(file); - } - catch - { - //no file extension specified: try using the default one - file += ".sub"; - } - - if (string.IsNullOrWhiteSpace(extension)) - { - extension = ".sub"; - file += ".sub"; - } - - if (extension == ".sub") - { - Stream stream = null; - try - { - stream = SaveUtil.DecompressFiletoStream(file); - } - catch (FileNotFoundException e) - { - exception = e; - DebugConsole.ThrowError("Loading submarine \"" + file + "\" failed! (File not found)"); - return null; - } - catch (Exception e) - { - exception = e; - DebugConsole.ThrowError("Loading submarine \"" + file + "\" failed!", e); - return null; - } - - try - { - stream.Position = 0; - doc = XDocument.Load(stream); //ToolBox.TryLoadXml(file); - stream.Close(); - stream.Dispose(); - } - - catch (Exception e) - { - exception = e; - DebugConsole.ThrowError("Loading submarine \"" + file + "\" failed! (" + e.Message + ")"); - return null; - } - } - else if (extension == ".xml") - { - try - { - ToolBox.IsProperFilenameCase(file); - doc = XDocument.Load(file, LoadOptions.SetBaseUri); - } - - catch (Exception e) - { - exception = e; - DebugConsole.ThrowError("Loading submarine \"" + file + "\" failed! (" + e.Message + ")"); - return null; - } - } - else - { - DebugConsole.ThrowError("Couldn't load submarine \"" + file + "! (Unrecognized file extension)"); - return null; - } - - return doc; - } - - public void Load(bool unloadPrevious, XElement submarineElement = null, bool showWarningMessages = true) - { - if (unloadPrevious) Unload(); - Loading = true; - if (submarineElement == null) - { - XDocument doc = null; - int maxLoadRetries = 4; - for (int i = 0; i <= maxLoadRetries; i++) - { - doc = OpenFile(filePath); - if (doc != null || i == maxLoadRetries || !File.Exists(filePath)) { break; } - DebugConsole.NewMessage("Loading the submarine \"" + Name + "\" failed, retrying in 250 ms..."); - Thread.Sleep(250); - } - if (doc == null || doc.Root == null) - { - IsFileCorrupted = true; - return; - } - submarineElement = doc.Root; - } + Info = new SubmarineInfo(info); + + ConnectedDockingPorts = new Dictionary(); - GameVersion = GameVersion ?? new Version(submarineElement.GetAttributeString("gameversion", "0.0.0.0")); - Description = submarineElement.GetAttributeString("description", ""); - Enum.TryParse(submarineElement.GetAttributeString("tags", ""), out tags); - //place the sub above the top of the level HiddenSubPosition = HiddenSubStartPosition; if (GameMain.GameSession != null && GameMain.GameSession.Level != null) @@ -1515,8 +1046,12 @@ namespace Barotrauma { IdOffset = Math.Max(IdOffset, me.ID); } - - var newEntities = MapEntity.LoadAll(this, submarineElement, filePath); + + List newEntities = new List(); + if (Info.SubmarineElement != null) + { + newEntities = MapEntity.LoadAll(this, Info.SubmarineElement, Info.FilePath); + } Vector2 center = Vector2.Zero; var matchingHulls = Hull.hullList.FindAll(h => h.Submarine == this); @@ -1558,10 +1093,53 @@ namespace Barotrauma MapEntity.mapEntityList[i].Move(-center); } } - } - subBody = new SubmarineBody(this, showWarningMessages); - subBody.SetPosition(HiddenSubPosition); + subBody = new SubmarineBody(this, showWarningMessages); + subBody.SetPosition(HiddenSubPosition); + + if (info.IsOutpost) + { + ShowSonarMarker = false; + PhysicsBody.FarseerBody.BodyType = BodyType.Static; + TeamID = Character.TeamType.FriendlyNPC; + + foreach (MapEntity me in MapEntity.mapEntityList) + { + if (me.Submarine != this) { continue; } + if (me is Item item) + { + if (item.GetComponent() != null) + { + item.Indestructible = true; + } + foreach (ItemComponent ic in item.Components) + { + if (ic is ConnectionPanel connectionPanel) + { + //prevent rewiring + connectionPanel.Locked = true; + } + else if (ic is Holdable holdable && holdable.Attached) + { + //prevent deattaching items from walls +#if CLIENT + if (GameMain.GameSession?.GameMode is TutorialMode) + { + continue; + } +#endif + holdable.CanBePicked = false; + holdable.CanBeSelected = false; + } + } + } + else if (me is Structure structure) + { + structure.Indestructible = true; + } + } + } + } loaded.Add(this); @@ -1581,10 +1159,17 @@ namespace Barotrauma Loading = false; MapEntity.MapLoaded(newEntities, true); + foreach (MapEntity me in MapEntity.mapEntityList) + { + if (me is LinkedSubmarine linkedSub && linkedSub.Submarine == this) + { + linkedSub.LinkDummyToMainSubmarine(); + } + } foreach (Hull hull in matchingHulls) { - if (string.IsNullOrEmpty(hull.RoomName) || !hull.RoomName.ToLowerInvariant().Contains("roomname.")) + if (string.IsNullOrEmpty(hull.RoomName) || !hull.RoomName.Contains("roomname.", StringComparison.OrdinalIgnoreCase)) { hull.RoomName = hull.CreateRoomName(); } @@ -1595,9 +1180,9 @@ namespace Barotrauma #endif //if the sub was made using an older version, //halve the brightness of the lights to make them look (almost) right on the new lighting formula - if (showWarningMessages && Screen.Selected != GameMain.SubEditorScreen && (GameVersion == null || GameVersion < new Version("0.8.9.0"))) + if (showWarningMessages && Screen.Selected != GameMain.SubEditorScreen && (Info.GameVersion == null || Info.GameVersion < new Version("0.8.9.0"))) { - DebugConsole.ThrowError("The submarine \"" + Name + "\" was made using an older version of the Barotrauma that used a different formula to calculate the lighting. " + DebugConsole.ThrowError("The submarine \"" + Info.Name + "\" was made using an older version of the Barotrauma that used a different formula to calculate the lighting. " + "The game automatically adjusts the lights make them look better with the new formula, but it's recommended to open the submarine in the submarine editor and make sure everything looks right after the automatic conversion."); foreach (Item item in Item.ItemList) { @@ -1608,91 +1193,51 @@ namespace Barotrauma } } - ID = (ushort)(ushort.MaxValue - 1 - Submarine.loaded.IndexOf(this)); } - public static Submarine Load(XElement element, bool unloadPrevious) + public static Submarine Load(SubmarineInfo info, bool unloadPrevious) { if (unloadPrevious) Unload(); - //tryload -> false - - Submarine sub = new Submarine(element.GetAttributeString("name", ""), "", false); - sub.Load(unloadPrevious, element); + Submarine sub = new Submarine(info, false); return sub; } - public static Submarine Load(string fileName, bool unloadPrevious) - { - return Load(fileName, SavePath, unloadPrevious); - } - - public static Submarine Load(string fileName, string folder, bool unloadPrevious) - { - if (unloadPrevious) Unload(); - - string path = string.IsNullOrWhiteSpace(folder) ? fileName : System.IO.Path.Combine(SavePath, fileName); - - Submarine sub = new Submarine(path); - sub.Load(unloadPrevious); - - return sub; - } - - public bool SaveAs(string filePath, MemoryStream previewImage = null) - { - name = Path.GetFileNameWithoutExtension(filePath); - - XDocument doc = new XDocument(new XElement("Submarine")); - SaveToXElement(doc.Root); - - if (previewImage != null) - { - doc.Root.Add(new XAttribute("previewimage", Convert.ToBase64String(previewImage.ToArray()))); - } - - try - { - SaveUtil.CompressStringToFile(filePath, doc.ToString()); - } - catch (Exception e) - { - DebugConsole.ThrowError("Saving submarine \"" + filePath + "\" failed!", e); - return false; - } - - hash = null; - hashTask = null; - Md5Hash.RemoveFromCache(filePath); - - return true; - } - public void SaveToXElement(XElement element) { - element.Add(new XAttribute("name", name)); - element.Add(new XAttribute("description", Description ?? "")); - element.Add(new XAttribute("tags", tags.ToString())); + element.Add(new XAttribute("name", Info.Name)); + element.Add(new XAttribute("description", Info.Description ?? "")); + element.Add(new XAttribute("tags", Info.Tags.ToString())); element.Add(new XAttribute("gameversion", GameMain.Version.ToString())); Rectangle dimensions = CalculateDimensions(); element.Add(new XAttribute("dimensions", XMLExtensions.Vector2ToString(dimensions.Size.ToVector2()))); - element.Add(new XAttribute("recommendedcrewsizemin", RecommendedCrewSizeMin)); - element.Add(new XAttribute("recommendedcrewsizemax", RecommendedCrewSizeMax)); - element.Add(new XAttribute("recommendedcrewexperience", RecommendedCrewExperience ?? "")); - element.Add(new XAttribute("requiredcontentpackages", string.Join(", ", RequiredContentPackages))); + element.Add(new XAttribute("recommendedcrewsizemin", Info.RecommendedCrewSizeMin)); + element.Add(new XAttribute("recommendedcrewsizemax", Info.RecommendedCrewSizeMax)); + element.Add(new XAttribute("recommendedcrewexperience", Info.RecommendedCrewExperience ?? "")); + element.Add(new XAttribute("requiredcontentpackages", string.Join(", ", Info.RequiredContentPackages))); - foreach (MapEntity e in MapEntity.mapEntityList) + foreach (MapEntity e in MapEntity.mapEntityList.OrderBy(e => e.ID)) { if (e.Submarine != this || !e.ShouldBeSaved) continue; + if (e is Item item && item.FindParentInventory(inv => inv is CharacterInventory) != null) continue; e.Save(element); } - CheckSubsLeftBehind(element); + Info.CheckSubsLeftBehind(element); } + public bool SaveAs(string filePath, MemoryStream previewImage = null) + { + var newInfo = new SubmarineInfo(this); + newInfo.FilePath = filePath; + newInfo.Name = Path.GetFileNameWithoutExtension(filePath); + Info.Dispose(); Info = newInfo; + + return newInfo.SaveAs(filePath, previewImage); + } public static bool Unloading { @@ -1710,7 +1255,8 @@ namespace Barotrauma if (GameMain.LightManager != null) GameMain.LightManager.ClearLights(); #endif - foreach (Submarine sub in loaded) + var _loaded = new List(loaded); + foreach (Submarine sub in _loaded) { sub.Remove(); } @@ -1756,21 +1302,25 @@ namespace Barotrauma subBody = null; + if (entityGrid != null) + { + Hull.EntityGrids.Remove(entityGrid); + entityGrid = null; + } + visibleEntities = null; if (MainSub == this) MainSub = null; if (MainSubs[1] == this) MainSubs[1] = null; ConnectedDockingPorts?.Clear(); + + loaded.Remove(this); } public void Dispose() { - savedSubmarines.Remove(this); -#if CLIENT - PreviewImage?.Remove(); - PreviewImage = null; -#endif + Remove(); } private List outdoorNodes; @@ -1785,6 +1335,7 @@ namespace Barotrauma return outdoorNodes; } } + private HashSet obstructedNodes = new HashSet(); /// @@ -1859,5 +1410,4 @@ namespace Barotrauma obstructedNodes.Clear(); } } - } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs index 15460c917..ffbfa18ff 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs @@ -131,7 +131,7 @@ namespace Barotrauma farseerBody.UserData = this; foreach (Structure wall in Structure.WallList) { - if (wall.Submarine != submarine) continue; + if (wall.Submarine != submarine || wall.IsPlatform) { continue; } Rectangle rect = wall.Rect; @@ -150,7 +150,7 @@ namespace Barotrauma foreach (Hull hull in Hull.hullList) { - if (hull.Submarine != submarine) continue; + if (hull.Submarine != submarine) { continue; } Rectangle rect = hull.Rect; farseerBody.CreateRectangle( @@ -167,7 +167,7 @@ namespace Barotrauma foreach (Item item in Item.ItemList) { - if (item.StaticBodyConfig == null || item.Submarine != submarine) continue; + if (item.StaticBodyConfig == null || item.Submarine != submarine) { continue; } float radius = item.StaticBodyConfig.GetAttributeFloat("radius", 0.0f) * item.Scale; float width = item.StaticBodyConfig.GetAttributeFloat("width", 0.0f) * item.Scale; @@ -180,7 +180,7 @@ namespace Barotrauma if (width > 0.0f && height > 0.0f) { - farseerBody.CreateRectangle(simWidth, simHeight, 5.0f, simPos).UserData = item; + item.StaticFixtures.Add(farseerBody.CreateRectangle(simWidth, simHeight, 5.0f, simPos)); minExtents.X = Math.Min(item.Position.X - width / 2, minExtents.X); minExtents.Y = Math.Min(item.Position.Y - height / 2, minExtents.Y); @@ -189,9 +189,9 @@ namespace Barotrauma } else if (radius > 0.0f && width > 0.0f) { - farseerBody.CreateRectangle(simWidth, simRadius * 2, 5.0f, simPos).UserData = item; - farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitX * simWidth / 2).UserData = item; - farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitX * simWidth / 2).UserData = item; + item.StaticFixtures.Add(farseerBody.CreateRectangle(simWidth, simRadius * 2, 5.0f, simPos)); + item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitX * simWidth / 2)); + item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitX * simWidth / 2)); minExtents.X = Math.Min(item.Position.X - width / 2 - radius, minExtents.X); minExtents.Y = Math.Min(item.Position.Y - radius, minExtents.Y); maxExtents.X = Math.Max(item.Position.X + width / 2 + radius, maxExtents.X); @@ -199,9 +199,9 @@ namespace Barotrauma } else if (radius > 0.0f && height > 0.0f) { - farseerBody.CreateRectangle(simRadius * 2, height, 5.0f, simPos).UserData = item; - farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitY * simHeight / 2).UserData = item; - farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitX * simHeight / 2).UserData = item; + item.StaticFixtures.Add(farseerBody.CreateRectangle(simRadius * 2, height, 5.0f, simPos)); + item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitY * simHeight / 2)); + item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitX * simHeight / 2)); minExtents.X = Math.Min(item.Position.X - radius, minExtents.X); minExtents.Y = Math.Min(item.Position.Y - height / 2 - radius, minExtents.Y); maxExtents.X = Math.Max(item.Position.X + radius, maxExtents.X); @@ -209,12 +209,13 @@ namespace Barotrauma } else if (radius > 0.0f) { - farseerBody.CreateCircle(simRadius, 5.0f, simPos).UserData = item; + item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos)); minExtents.X = Math.Min(item.Position.X - radius, minExtents.X); minExtents.Y = Math.Min(item.Position.Y - radius, minExtents.Y); maxExtents.X = Math.Max(item.Position.X + radius, maxExtents.X); maxExtents.Y = Math.Max(item.Position.Y + radius, maxExtents.Y); } + item.StaticFixtures.ForEach(f => f.UserData = item); } Borders = new Rectangle((int)minExtents.X, (int)maxExtents.Y, (int)(maxExtents.X - minExtents.X), (int)(maxExtents.Y - minExtents.Y)); @@ -521,9 +522,16 @@ namespace Barotrauma Vector2 normalizedVel = character.AnimController.Collider.LinearVelocity == Vector2.Zero ? Vector2.Zero : Vector2.Normalize(character.AnimController.Collider.LinearVelocity); - Vector2 targetPos = ConvertUnits.ToDisplayUnits(points[0] - contactNormal); + //try to find the hull right next to the contact point + Vector2 targetPos = ConvertUnits.ToDisplayUnits(points[0] - contactNormal * 0.1f); Hull newHull = Hull.FindHull(targetPos, null); - + //not found, try searching a bit further + if (newHull == null) + { + targetPos = ConvertUnits.ToDisplayUnits(points[0] - contactNormal); + newHull = Hull.FindHull(targetPos, null); + } + //still not found, try searching in the direction the character is heading to if (newHull == null) { targetPos = ConvertUnits.ToDisplayUnits(points[0] + normalizedVel); @@ -771,7 +779,7 @@ namespace Barotrauma if (Character.Controlled != null && Character.Controlled.Submarine == submarine) { GameMain.GameScreen.Cam.Shake = impact * 2.0f; - if (!submarine.IsOutpost && !submarine.DockedTo.Any(s => s.IsOutpost)) + if (submarine.Info.Type == SubmarineInfo.SubmarineType.Player && !submarine.DockedTo.Any(s => s.Info.Type != SubmarineInfo.SubmarineType.Player)) { float angularVelocity = (impactPos.X - Body.SimPosition.X) / ConvertUnits.ToSimUnits(submarine.Borders.Width / 2) * impulse.Y diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs new file mode 100644 index 000000000..efcca37a5 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineInfo.cs @@ -0,0 +1,611 @@ +using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; + +namespace Barotrauma +{ + [Flags] + public enum SubmarineTag + { + [Description("Shuttle")] + Shuttle = 1, + [Description("Hide in menus")] + HideInMenus = 2 + } + + partial class SubmarineInfo : IDisposable + { + public const string SavePath = "Submarines"; + + private static List savedSubmarines = new List(); + public static IEnumerable SavedSubmarines + { + get { return savedSubmarines; } + } + + private Task hashTask; + private Md5Hash hash; + + public readonly DateTime LastModifiedTime; + + public SubmarineTag Tags { get; private set; } + + public int RecommendedCrewSizeMin = 1, RecommendedCrewSizeMax = 2; + public string RecommendedCrewExperience; + + public HashSet RequiredContentPackages = new HashSet(); + + public string Name + { + get; + set; + } + + public string DisplayName + { + get; + set; + } + + public string Description + { + get; + set; + } + + public Version GameVersion + { + get; + set; + } + + public bool IsOutpost => Type == SubmarineType.Outpost; + public bool IsWreck => Type == SubmarineType.Wreck; + + public bool IsPlayer => Type == SubmarineType.Player; + + public enum SubmarineType { Player, Outpost, Wreck } + public SubmarineType Type { get; set; } + + public Md5Hash MD5Hash + { + get + { + if (hash == null) + { + XDocument doc = OpenFile(FilePath); + StartHashDocTask(doc); + hashTask.Wait(); + hashTask = null; + } + + return hash; + } + } + + public Vector2 Dimensions + { + get; + private set; + } + + public string FilePath + { + get; + set; + } + + public readonly XElement SubmarineElement; + + public override string ToString() + { + return "Barotrauma.SubmarineInfo (" + Name + ")"; + } + + public bool IsFileCorrupted + { + get; + private set; + } + + private bool? requiredContentPackagesInstalled; + public bool RequiredContentPackagesInstalled + { + get + { + if (requiredContentPackagesInstalled.HasValue) { return requiredContentPackagesInstalled.Value; } + return RequiredContentPackages.All(cp => GameMain.SelectedPackages.Any(cp2 => cp2.Name == cp)); + } + set + { + requiredContentPackagesInstalled = value; + } + } + + private bool? subsLeftBehind; + public bool SubsLeftBehind + { + get + { + if (subsLeftBehind.HasValue) { return subsLeftBehind.Value; } + CheckSubsLeftBehind(SubmarineElement); + return subsLeftBehind.Value; + } + } + + public bool LeftBehindSubDockingPortOccupied + { + get; private set; + } + + //constructors & generation ---------------------------------------------------- + public SubmarineInfo() + { + FilePath = null; + Name = DisplayName = TextManager.Get("UnspecifiedSubFileName"); + IsFileCorrupted = false; + RequiredContentPackages = new HashSet(); + } + + public SubmarineInfo(string filePath, string hash = "", XElement element = null, bool tryLoad = true) + { + FilePath = filePath; + if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath)) + { + LastModifiedTime = File.GetLastWriteTime(filePath); + } + try + { + Name = DisplayName = Path.GetFileNameWithoutExtension(filePath); + } + catch (Exception e) + { + DebugConsole.ThrowError("Error loading submarine " + filePath + "!", e); + } + + if (!string.IsNullOrWhiteSpace(hash)) + { + this.hash = new Md5Hash(hash); + } + + IsFileCorrupted = false; + + RequiredContentPackages = new HashSet(); + + if (element == null && tryLoad) + { + XDocument doc = null; + int maxLoadRetries = 4; + for (int i = 0; i <= maxLoadRetries; i++) + { + doc = OpenFile(filePath, out Exception e); + if (e != null && !(e is IOException)) { break; } + if (doc != null || i == maxLoadRetries || !File.Exists(filePath)) { break; } + DebugConsole.NewMessage("Opening submarine file \"" + filePath + "\" failed, retrying in 250 ms..."); + Thread.Sleep(250); + } + if (doc == null || doc.Root == null) + { + IsFileCorrupted = true; + return; + } + + if (string.IsNullOrWhiteSpace(hash)) + { + StartHashDocTask(doc); + } + + SubmarineElement = doc.Root; + } + else + { + SubmarineElement = element; + } + + Name = SubmarineElement.GetAttributeString("name", null) ?? Name; + + Init(); + } + + public SubmarineInfo(Submarine sub) : this(sub.Info) + { + SubmarineElement = new XElement("Submarine"); + sub.SaveToXElement(SubmarineElement); + Init(); + } + + public SubmarineInfo(SubmarineInfo original) + { + Name = original.Name; + DisplayName = original.DisplayName; + Description = original.Description; + GameVersion = original.GameVersion; + Type = original.Type; + hash = !string.IsNullOrEmpty(original.FilePath) ? original.MD5Hash : null; + Dimensions = original.Dimensions; + FilePath = original.FilePath; + RequiredContentPackages = new HashSet(original.RequiredContentPackages); + IsFileCorrupted = original.IsFileCorrupted; + SubmarineElement = original.SubmarineElement; + RecommendedCrewExperience = original.RecommendedCrewExperience; + RecommendedCrewSizeMin = original.RecommendedCrewSizeMin; + RecommendedCrewSizeMax = original.RecommendedCrewSizeMax; + Tags = original.Tags; +#if CLIENT + PreviewImage = original.PreviewImage != null ? new Sprite(original.PreviewImage.Texture, null, null) : null; +#endif + } + + private void Init() + { + DisplayName = TextManager.Get("Submarine.Name." + Name, true); + if (string.IsNullOrEmpty(DisplayName)) { DisplayName = Name; } + + Description = TextManager.Get("Submarine.Description." + Name, true); + if (string.IsNullOrEmpty(Description)) { Description = SubmarineElement.GetAttributeString("description", ""); } + + GameVersion = new Version(SubmarineElement.GetAttributeString("gameversion", "0.0.0.0")); + if (Enum.TryParse(SubmarineElement.GetAttributeString("tags", ""), out SubmarineTag tags)) + { + Tags = tags; + } + Dimensions = SubmarineElement.GetAttributeVector2("dimensions", Vector2.Zero); + RecommendedCrewSizeMin = SubmarineElement.GetAttributeInt("recommendedcrewsizemin", 0); + RecommendedCrewSizeMax = SubmarineElement.GetAttributeInt("recommendedcrewsizemax", 0); + RecommendedCrewExperience = SubmarineElement.GetAttributeString("recommendedcrewexperience", "Unknown"); + + //backwards compatibility (use text tags instead of the actual text) + if (RecommendedCrewExperience == "Beginner") + { + RecommendedCrewExperience = "CrewExperienceLow"; + } + else if (RecommendedCrewExperience == "Intermediate") + { + RecommendedCrewExperience = "CrewExperienceMid"; + } + else if (RecommendedCrewExperience == "Experienced") + { + RecommendedCrewExperience = "CrewExperienceHigh"; + } + + RequiredContentPackages.Clear(); + string[] contentPackageNames = SubmarineElement.GetAttributeStringArray("requiredcontentpackages", new string[0]); + foreach (string contentPackageName in contentPackageNames) + { + RequiredContentPackages.Add(contentPackageName); + } + + InitProjectSpecific(); + } + + partial void InitProjectSpecific(); + + public void Dispose() + { + if (savedSubmarines.Contains(this)) { savedSubmarines.Remove(this); } + } + + public bool IsVanillaSubmarine() + { + var vanilla = GameMain.VanillaContent; + if (vanilla != null) + { + var vanillaSubs = vanilla.GetFilesOfType(ContentType.Submarine); + string pathToCompare = FilePath.Replace(@"\", @"/").ToLowerInvariant(); + if (vanillaSubs.Any(sub => sub.Replace(@"\", @"/").ToLowerInvariant() == pathToCompare)) + { + return true; + } + } + return false; + } + + public void StartHashDocTask(XDocument doc) + { + if (hash != null) { return; } + if (hashTask != null) { return; } + + hashTask = new Task(() => + { + hash = new Md5Hash(doc, FilePath); + }); + hashTask.Start(); + } + + public bool HasTag(SubmarineTag tag) + { + return Tags.HasFlag(tag); + } + + public void AddTag(SubmarineTag tag) + { + if (Tags.HasFlag(tag)) return; + + Tags |= tag; + } + + public void RemoveTag(SubmarineTag tag) + { + if (!Tags.HasFlag(tag)) return; + + Tags &= ~tag; + } + + public void CheckSubsLeftBehind(XElement element = null) + { + if (element == null) { element = SubmarineElement; } + + subsLeftBehind = false; + LeftBehindSubDockingPortOccupied = false; + foreach (XElement subElement in element.Elements()) + { + if (!subElement.Name.ToString().Equals("linkedsubmarine", StringComparison.OrdinalIgnoreCase)) { continue; } + if (subElement.Attribute("location") == null) { continue; } + + subsLeftBehind = true; + ushort targetDockingPortID = (ushort)subElement.GetAttributeInt("originallinkedto", 0); + XElement targetPortElement = targetDockingPortID == 0 ? null : + element.Elements().FirstOrDefault(e => e.GetAttributeInt("ID", 0) == targetDockingPortID); + if (targetPortElement != null && targetPortElement.GetAttributeIntArray("linked", new int[0]).Length > 0) + { + LeftBehindSubDockingPortOccupied = true; + } + } + } + + + //saving/loading ---------------------------------------------------- + public bool SaveAs(string filePath, MemoryStream previewImage=null) + { + var newElement = new XElement(SubmarineElement.Name, + SubmarineElement.Attributes().Where(a => !string.Equals(a.Name.LocalName, "previewimage", StringComparison.InvariantCultureIgnoreCase)), + SubmarineElement.Elements()); + XDocument doc = new XDocument(newElement); + + if (previewImage != null) + { + doc.Root.Add(new XAttribute("previewimage", Convert.ToBase64String(previewImage.ToArray()))); + } + + try + { + SaveUtil.CompressStringToFile(filePath, doc.ToString()); + } + catch (Exception e) + { + DebugConsole.ThrowError("Saving submarine \"" + filePath + "\" failed!", e); + return false; + } + + return true; + } + + public static void AddToSavedSubs(SubmarineInfo subInfo) + { + savedSubmarines.Add(subInfo); + } + + public static void RefreshSavedSub(string filePath) + { + string fullPath = Path.GetFullPath(filePath); + for (int i = savedSubmarines.Count - 1; i >= 0; i--) + { + if (Path.GetFullPath(savedSubmarines[i].FilePath) == fullPath) + { + savedSubmarines[i].Dispose(); + } + } + + if (File.Exists(filePath)) + { + var subInfo = new SubmarineInfo(filePath); + if (!subInfo.IsFileCorrupted) + { + savedSubmarines.Add(subInfo); + } + savedSubmarines = savedSubmarines.OrderBy(s => s.FilePath ?? "").ToList(); + } + } + + public static void RefreshSavedSubs() + { + var contentPackageSubs = ContentPackage.GetFilesOfType(GameMain.Config.SelectedContentPackages, ContentType.Submarine); + + for (int i = savedSubmarines.Count - 1; i >= 0; i--) + { + if (File.Exists(savedSubmarines[i].FilePath) && + savedSubmarines[i].LastModifiedTime == File.GetLastWriteTime(savedSubmarines[i].FilePath) && + (Path.GetFullPath(Path.GetDirectoryName(savedSubmarines[i].FilePath)) == Path.GetFullPath(SavePath) || + contentPackageSubs.Any(fp => Path.GetFullPath(fp.Path).CleanUpPath() == Path.GetFullPath(savedSubmarines[i].FilePath).CleanUpPath()))) + { + continue; + } + savedSubmarines[i].Dispose(); + } + + if (!Directory.Exists(SavePath)) + { + try + { + Directory.CreateDirectory(SavePath); + } + catch (Exception e) + { + DebugConsole.ThrowError("Directory \"" + SavePath + "\" not found and creating the directory failed.", e); + return; + } + } + + List filePaths; + string[] subDirectories; + + try + { + filePaths = Directory.GetFiles(SavePath).ToList(); + subDirectories = Directory.GetDirectories(SavePath).Where(s => + { + DirectoryInfo dir = new DirectoryInfo(s); + return (dir.Attributes & FileAttributes.Hidden) == 0; + }).ToArray(); + } + catch (Exception e) + { + DebugConsole.ThrowError("Couldn't open directory \"" + SavePath + "\"!", e); + return; + } + + foreach (string subDirectory in subDirectories) + { + try + { + filePaths.AddRange(Directory.GetFiles(subDirectory).ToList()); + } + catch (Exception e) + { + DebugConsole.ThrowError("Couldn't open subdirectory \"" + subDirectory + "\"!", e); + return; + } + } + + foreach (ContentFile subFile in contentPackageSubs) + { + if (!filePaths.Any(fp => Path.GetFullPath(fp) == Path.GetFullPath(subFile.Path))) + { + filePaths.Add(subFile.Path); + } + } + + filePaths.RemoveAll(p => savedSubmarines.Any(sub => sub.FilePath == p)); + + foreach (string path in filePaths) + { + var subInfo = new SubmarineInfo(path); + if (subInfo.IsFileCorrupted) + { +#if CLIENT + if (DebugConsole.IsOpen) { DebugConsole.Toggle(); } + var deleteSubPrompt = new GUIMessageBox( + TextManager.Get("Error"), + TextManager.GetWithVariable("SubLoadError", "[subname]", subInfo.Name) + "\n" + + TextManager.GetWithVariable("DeleteFileVerification", "[filename]", subInfo.Name), + new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); + + string filePath = path; + deleteSubPrompt.Buttons[0].OnClicked += (btn, userdata) => + { + try + { + File.Delete(filePath); + } + catch (Exception e) + { + DebugConsole.ThrowError($"Failed to delete file \"{filePath}\".", e); + } + deleteSubPrompt.Close(); + return true; + }; + deleteSubPrompt.Buttons[1].OnClicked += deleteSubPrompt.Close; +#endif + } + else + { + savedSubmarines.Add(subInfo); + } + } + } + + static readonly string TempFolder = Path.Combine("Submarine", "Temp"); + + public static XDocument OpenFile(string file) + { + return OpenFile(file, out _); + } + + public static XDocument OpenFile(string file, out Exception exception) + { + XDocument doc = null; + string extension = ""; + exception = null; + + try + { + extension = System.IO.Path.GetExtension(file); + } + catch + { + //no file extension specified: try using the default one + file += ".sub"; + } + + if (string.IsNullOrWhiteSpace(extension)) + { + extension = ".sub"; + file += ".sub"; + } + + if (extension == ".sub") + { + Stream stream = null; + try + { + stream = SaveUtil.DecompressFiletoStream(file); + } + catch (FileNotFoundException e) + { + exception = e; + DebugConsole.ThrowError("Loading submarine \"" + file + "\" failed! (File not found) " + Environment.StackTrace, e); + return null; + } + catch (Exception e) + { + exception = e; + DebugConsole.ThrowError("Loading submarine \"" + file + "\" failed!", e); + return null; + } + + try + { + stream.Position = 0; + doc = XDocument.Load(stream); //ToolBox.TryLoadXml(file); + stream.Close(); + stream.Dispose(); + } + + catch (Exception e) + { + exception = e; + DebugConsole.ThrowError("Loading submarine \"" + file + "\" failed! (" + e.Message + ")"); + return null; + } + } + else if (extension == ".xml") + { + try + { + ToolBox.IsProperFilenameCase(file); + doc = XDocument.Load(file, LoadOptions.SetBaseUri); + } + + catch (Exception e) + { + exception = e; + DebugConsole.ThrowError("Loading submarine \"" + file + "\" failed! (" + e.Message + ")"); + return null; + } + } + else + { + DebugConsole.ThrowError("Couldn't load submarine \"" + file + "! (Unrecognized file extension)"); + return null; + } + + return doc; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs index c8a2b2291..c3fe670a4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/WayPoint.cs @@ -4,13 +4,14 @@ using FarseerPhysics.Dynamics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Linq; using System.Xml.Linq; +using Barotrauma.RuinGeneration; +using Barotrauma.Extensions; namespace Barotrauma { - public enum SpawnType { Path, Human, Enemy, Cargo }; + public enum SpawnType { Path = 0, Human = 1, Enemy = 2, Cargo = 3, Corpse = 4 }; partial class WayPoint : MapEntity { public static List WayPointList = new List(); @@ -121,9 +122,16 @@ namespace Barotrauma idCardTags = new string[0]; #if CLIENT - if (iconTexture == null) + if (iconSprites == null) { - iconTexture = Sprite.LoadTexture("Content/Map/waypointIcons.png"); + iconSprites = new Dictionary() + { + { SpawnType.Path, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(0,0,128,128)) }, + { SpawnType.Human, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(128,0,128,128)) }, + { SpawnType.Enemy, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(256,0,128,128)) }, + { SpawnType.Cargo, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(384,0,128,128)) }, + { SpawnType.Corpse, new Sprite("Content/UI/MainIconsAtlas.png", new Rectangle(512,0,128,128)) } + }; } #endif @@ -148,21 +156,12 @@ namespace Barotrauma return clone; } - public override bool IsMouseOn(Vector2 position) - { -#if CLIENT - if (IsHidden()) return false; -#endif - - return base.IsMouseOn(position); - } - - public static void GenerateSubWaypoints(Submarine submarine) + public static bool GenerateSubWaypoints(Submarine submarine) { if (!Hull.hullList.Any()) { DebugConsole.ThrowError("Couldn't generate waypoints: no hulls found."); - return; + return false; } List existingWaypoints = WayPointList.FindAll(wp => wp.spawnType == SpawnType.Path); @@ -465,6 +464,8 @@ namespace Barotrauma { door.Body.Enabled = false; } + + return true; } private WayPoint FindClosest(int dir, bool horizontalSearch, Vector2 tolerance, Body ignoredBody = null) @@ -520,22 +521,14 @@ namespace Barotrauma if (!wayPoint2.linkedTo.Contains(this)) wayPoint2.linkedTo.Add(this); } - public static WayPoint GetRandom(SpawnType spawnType = SpawnType.Human, Job assignedJob = null, Submarine sub = null, bool useSyncedRand = false) + public static WayPoint GetRandom(SpawnType spawnType = SpawnType.Human, Job assignedJob = null, Submarine sub = null, Ruin ruin = null, bool useSyncedRand = false) { - List wayPoints = new List(); - - foreach (WayPoint wp in WayPointList) - { - if (sub != null && wp.Submarine != sub) continue; - if (wp.spawnType != spawnType) continue; - if (assignedJob != null && wp.assignedJob != assignedJob.Prefab) continue; - - wayPoints.Add(wp); - } - - if (!wayPoints.Any()) return null; - - return wayPoints[Rand.Int(wayPoints.Count, (useSyncedRand ? Rand.RandSync.Server : Rand.RandSync.Unsynced))]; + return WayPointList.GetRandom(wp => + wp.Submarine == sub && + wp.ParentRuin == ruin && + wp.spawnType == spawnType && + (assignedJob == null || (assignedJob != null && wp.assignedJob == assignedJob.Prefab)) + , useSyncedRand ? Rand.RandSync.Server : Rand.RandSync.Unsynced); } public static WayPoint[] SelectCrewSpawnPoints(List crew, Submarine submarine) @@ -584,7 +577,7 @@ namespace Barotrauma if (assignedWayPoints[i] != null) continue; //everything else failed -> just give a random spawnpoint inside the sub - assignedWayPoints[i] = GetRandom(SpawnType.Human, null, submarine, true); + assignedWayPoints[i] = GetRandom(SpawnType.Human, null, submarine, useSyncedRand: true); } for (int i = 0; i < assignedWayPoints.Length; i++) @@ -654,7 +647,7 @@ namespace Barotrauma { w.assignedJob = JobPrefab.Get(jobIdentifier) ?? - JobPrefab.Prefabs.Find(jp => jp.Name.ToLowerInvariant() == jobIdentifier); + JobPrefab.Prefabs.Find(jp => jp.Name.Equals(jobIdentifier, StringComparison.OrdinalIgnoreCase)); } w.ladderId = (ushort)element.GetAttributeInt("ladders", 0); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs index b2bc4db8f..53e2f4bee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChatMessage.cs @@ -11,6 +11,8 @@ namespace Barotrauma.Networking Default, Error, Dead, Server, Radio, Private, Console, MessageBox, Order, ServerLog, ServerMessageBox } + public enum PlayerConnectionChangeType { None = 0, Joined = 1, Kicked = 2, Disconnected = 3, Banned = 4 } + partial class ChatMessage { public const int MaxLength = 150; @@ -58,8 +60,10 @@ namespace Barotrauma.Networking } public ChatMessageType Type; + public PlayerConnectionChangeType ChangeType; public readonly Character Sender; + public readonly Client SenderClient; public readonly string SenderName; @@ -89,19 +93,21 @@ namespace Barotrauma.Networking set; } - protected ChatMessage(string senderName, string text, ChatMessageType type, Character sender) + protected ChatMessage(string senderName, string text, ChatMessageType type, Character sender, Client client, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None) { Text = text; Type = type; Sender = sender; + SenderClient = client; SenderName = senderName; + ChangeType = changeType; } - public static ChatMessage Create(string senderName, string text, ChatMessageType type, Character sender) + public static ChatMessage Create(string senderName, string text, ChatMessageType type, Character sender, Client client = null, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None) { - return new ChatMessage(senderName, text, type, sender); + return new ChatMessage(senderName, text, type, sender, client ?? GameMain.NetworkMember?.ConnectedClients?.Find(c => c.Character == sender), changeType); } public static string GetChatMessageCommand(string message, out string messageWithoutCommand) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs index f6e856946..0ece35d95 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs @@ -233,7 +233,7 @@ namespace Barotrauma.Networking { writeStream?.Write(msg, 0, msg.Length); } - catch (IOException e) + catch (IOException) { shutDown = true; break; @@ -263,7 +263,7 @@ namespace Barotrauma.Networking lengthBytes[1] = (byte)0; writeStream?.Write(lengthBytes, 0, 2); } - catch (IOException e) + catch (IOException) { shutDown = true; break; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs index c46241964..16cc90dd4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Client.cs @@ -14,6 +14,8 @@ namespace Barotrauma.Networking public byte ID; public UInt64 SteamID; + public UInt16 Ping; + public string PreferredJob; public Character.TeamType TeamID; @@ -86,6 +88,14 @@ namespace Barotrauma.Networking } } + public bool Spectating + { + get + { + return inGame && character == null; + } + } + private bool muted; public bool Muted { @@ -105,6 +115,8 @@ namespace Barotrauma.Networking } } + public bool HasPermissions = false; + public VoipQueue VoipQueue { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs index c1fbc3ac3..f84dc043a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ClientPermissions.cs @@ -51,7 +51,7 @@ namespace Barotrauma.Networking { foreach (XElement subElement in element.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "command") continue; + if (!subElement.Name.ToString().Equals("command", StringComparison.OrdinalIgnoreCase)) { continue; } string commandName = subElement.GetAttributeString("name", ""); DebugConsole.Command command = DebugConsole.FindCommand(commandName); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs index 363421c85..a99e369a1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/EntitySpawner.cs @@ -125,7 +125,7 @@ namespace Barotrauma { public readonly Entity Entity; - public readonly UInt16 OriginalID; + public readonly UInt16 OriginalID, OriginalInventoryID; public readonly bool Remove = false; @@ -133,6 +133,10 @@ namespace Barotrauma { Entity = entity; OriginalID = entity.ID; + if (entity is Item item && item.ParentInventory?.Owner != null) + { + OriginalInventoryID = item.ParentInventory.Owner.ID; + } Remove = remove; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/KarmaManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/KarmaManager.cs index bea21a9b3..001826a89 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/KarmaManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/KarmaManager.cs @@ -53,8 +53,20 @@ namespace Barotrauma [Serialize(0.25f, true)] public float DamageFriendlyKarmaDecrease { get; set; } + [Serialize(0.25f, true)] + public float StunFriendlyKarmaDecrease { get; set; } + + [Serialize(0.3f, true)] + public float StunFriendlyKarmaDecreaseThreshold { get; set; } + [Serialize(1.0f, true)] public float ExtinguishFireKarmaIncrease { get; set; } + + [Serialize(defaultValue: 15.0f, true)] + public float DangerousItemStealKarmaDecrease { get; set; } + + [Serialize(defaultValue: false, true)] + public bool DangerousItemStealBots { get; set; } private int allowedWireDisconnectionsPerMinute; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs index 6fd92e83f..5fca74a45 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetConfig.cs @@ -49,6 +49,26 @@ namespace Barotrauma.Networking public const int MaxEventPacketsPerUpdate = 4; + /// + /// How long the server waits for the clients to get in sync after the round has started before kicking them + /// + public const float RoundStartSyncDuration = 60.0f; + + /// + /// How long the server keeps events that everyone currently synced has received + /// + public const float EventRemovalTime = 15.0f; + + /// + /// If a client hasn't received an event that has been succesfully sent to someone within this time, they get kicked + /// + public const float OldReceivedEventKickTime = 10.0f; + + /// + /// If a client hasn't received an event after this time, they get kicked + /// + public const float OldEventKickTime = 30.0f; + /// /// Interpolates the positional error of a physics body towards zero. /// diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs index 1cac0fe43..f4a9431ea 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetEntityEvent/NetEntityEvent.cs @@ -15,7 +15,8 @@ namespace Barotrauma.Networking ChangeProperty, Control, UpdateSkills, - Combine + Combine, + ExecuteAttack } public readonly Entity Entity; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs index 5858d7f55..217df7536 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/NetworkMember.cs @@ -19,6 +19,8 @@ namespace Barotrauma.Networking VOICE, + PING_RESPONSE, + RESPONSE_STARTGAME, //tell the server whether you're ready to start SERVER_COMMAND, //tell the server to end a round or kick/ban someone (special permissions required) @@ -60,6 +62,9 @@ namespace Barotrauma.Networking VOICE, + PING_REQUEST, //ping the client + CLIENT_PINGS, //tell the client the pings of all other clients + QUERY_STARTGAME, //ask the clients whether they're ready to start STARTGAME, //start a new round STARTGAMEFINALIZE, //finalize round initialization @@ -212,9 +217,9 @@ namespace Barotrauma.Networking return radioComponent.HasRequiredContainedItems(sender, addMessage: false); } - public void AddChatMessage(string message, ChatMessageType type, string senderName = "", Character senderCharacter = null) + public void AddChatMessage(string message, ChatMessageType type, string senderName = "", Character senderCharacter = null, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None) { - AddChatMessage(ChatMessage.Create(senderName, message, type, senderCharacter)); + AddChatMessage(ChatMessage.Create(senderName, message, type, senderCharacter, changeType: changeType)); } public virtual void AddChatMessage(ChatMessage message) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs index 32e4d483b..76243edbc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/OrderChatMessage.cs @@ -25,7 +25,7 @@ namespace Barotrauma.Networking } public OrderChatMessage(Order order, string orderOption, string text, Entity targetEntity, Character targetCharacter, Character sender) - : base(sender?.Name, text, ChatMessageType.Order, sender) + : base(sender?.Name, text, ChatMessageType.Order, sender, GameMain.NetworkMember.ConnectedClients.Find(c => c.Character == sender)) { Order = order; OrderOption = orderOption; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs index a42d9ce20..26b5da467 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs @@ -63,15 +63,14 @@ namespace Barotrauma.Networking public Submarine RespawnShuttle { get; private set; } - public RespawnManager(NetworkMember networkMember, Submarine shuttle) - : base(shuttle) + public RespawnManager(NetworkMember networkMember, SubmarineInfo shuttleInfo) + : base(null) { this.networkMember = networkMember; - if (shuttle != null) + if (shuttleInfo != null) { - RespawnShuttle = new Submarine(shuttle.FilePath, shuttle.MD5Hash.Hash, true); - RespawnShuttle.Load(false); + RespawnShuttle = new Submarine(shuttleInfo, true); RespawnShuttle.PhysicsBody.FarseerBody.OnCollision += OnShuttleCollision; //prevent wifi components from communicating between the respawn shuttle and other subs diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs index d656a8baf..5a0d44c5c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerLog.cs @@ -11,7 +11,9 @@ namespace Barotrauma.Networking private struct LogMessage { public readonly string Text; + public readonly string SanitizedText; public readonly MessageType Type; + public readonly List RichData; public LogMessage(string text, MessageType type) { @@ -23,6 +25,7 @@ namespace Barotrauma.Networking { Text = $"[{DateTime.Now.ToString()}]\n {TextManager.GetServerMessage(text)}"; } + RichData = RichTextData.GetRichTextData(Text, out SanitizedText); Type = type; } @@ -38,6 +41,7 @@ namespace Barotrauma.Networking Wiring, ServerMessage, ConsoleUsage, + Karma, Error, } @@ -51,6 +55,7 @@ namespace Barotrauma.Networking { MessageType.Wiring, new Color(255, 157, 85) }, { MessageType.ServerMessage, new Color(157, 225, 160) }, { MessageType.ConsoleUsage, new Color(0, 162, 232) }, + { MessageType.Karma, new Color(75, 88, 255) }, { MessageType.Error, Color.Red }, }; @@ -64,6 +69,7 @@ namespace Barotrauma.Networking { MessageType.Wiring, "Wiring" }, { MessageType.ServerMessage, "ServerMessage" }, { MessageType.ConsoleUsage, "ConsoleUsage" }, + { MessageType.Karma, "Karma" }, { MessageType.Error, "Error" } }; @@ -101,12 +107,12 @@ namespace Barotrauma.Networking { //string logLine = "[" + DateTime.Now.ToLongTimeString() + "] " + line; + var newText = new LogMessage(line, messageType); + #if SERVER - DebugConsole.NewMessage(line, messageColor[messageType]); //TODO: REMOVE + DebugConsole.NewMessage(newText.SanitizedText, messageColor[messageType]); //TODO: REMOVE #endif - var newText = new LogMessage(line, messageType); - lines.Enqueue(newText); #if CLIENT @@ -134,7 +140,7 @@ namespace Barotrauma.Networking #if CLIENT while (listBox != null && listBox.Content.CountChildren > LinesPerFile) { - listBox.RemoveChild(listBox.Content.Children.First()); + listBox.RemoveChild(reverseOrder ? listBox.Content.Children.First() : listBox.Content.Children.Last()); } #endif } @@ -167,7 +173,7 @@ namespace Barotrauma.Networking try { - File.WriteAllLines(filePath, lines.Select(l => l.Text)); + File.WriteAllLines(filePath, lines.Select(l => l.SanitizedText)); } catch (Exception e) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs index aecaf8011..49d1c1ecf 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs @@ -138,7 +138,7 @@ namespace Barotrauma.Networking { return (a == null) == (b == null); } - return a.ToString().Equals(b.ToString(), StringComparison.InvariantCulture); + return a.ToString().Equals(b.ToString(), StringComparison.OrdinalIgnoreCase); } } @@ -608,6 +608,13 @@ namespace Barotrauma.Networking set; } + [Serialize(false, true)] + public bool DisableBotConversations + { + get; + set; + } + public float SelectedLevelDifficulty { get { return selectedLevelDifficulty; } @@ -759,7 +766,7 @@ namespace Barotrauma.Networking private set; } - [Serialize(120.0f, true)] + [Serialize(600.0f, true)] public float KickAFKTime { get; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs index bbfe0948f..8b45a594c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/SteamManager.cs @@ -1,6 +1,8 @@ -using System; +using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.InteropServices; #if USE_STEAM namespace Barotrauma.Steam @@ -19,6 +21,8 @@ namespace Barotrauma.Steam public const string MetadataFileName = "filelist.xml"; + public const string CopyIndicatorFileName = ".copying"; + private static readonly Dictionary tagCommonness = new Dictionary() { { "submarine", 10 }, @@ -45,7 +49,7 @@ namespace Barotrauma.Steam private static bool isInitialized; public static bool IsInitialized => isInitialized; - + public static void Initialize() { InitializeProjectSpecific(); @@ -70,27 +74,28 @@ namespace Barotrauma.Steam return Steamworks.SteamClient.Name; } - public static void OverlayCustomURL(string url) - { - if (!isInitialized || !Steamworks.SteamClient.IsValid) - { - return; - } - - Steamworks.SteamFriends.OpenWebOverlay(url); - } - - public static bool UnlockAchievement(string achievementName) + public static bool OverlayCustomURL(string url) { if (!isInitialized || !Steamworks.SteamClient.IsValid) { return false; } - DebugConsole.Log("Unlocked achievement \"" + achievementName + "\""); + Steamworks.SteamFriends.OpenWebOverlay(url); + return true; + } + + public static bool UnlockAchievement(string achievementIdentifier) + { + if (!isInitialized || !Steamworks.SteamClient.IsValid) + { + return false; + } + + DebugConsole.Log("Unlocked achievement \"" + achievementIdentifier + "\""); var achievements = Steamworks.SteamUserStats.Achievements.ToList(); - int achIndex = achievements.FindIndex(ach => ach.Name == achievementName); + int achIndex = achievements.FindIndex(ach => ach.Identifier == achievementIdentifier); bool unlocked = achIndex >= 0 ? achievements[achIndex].Trigger() : false; if (!unlocked) { @@ -99,7 +104,7 @@ namespace Barotrauma.Steam //(discovered[whateverbiomewasentered], kill[withwhateveritem], kill[somemonster] etc) so that we can add //some types of new achievements without the need for client-side changes. #if DEBUG - DebugConsole.NewMessage("Failed to unlock achievement \"" + achievementName + "\"."); + DebugConsole.NewMessage("Failed to unlock achievement \"" + achievementIdentifier + "\"."); #endif } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voip/VoipQueue.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voip/VoipQueue.cs index 5eddf719b..6e9ab0a9d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Voip/VoipQueue.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Voip/VoipQueue.cs @@ -57,6 +57,12 @@ namespace Barotrauma.Networking protected set; } + public bool ForceLocal + { + get; + set; + } + public DateTime LastReadTime { get; @@ -76,7 +82,7 @@ namespace Barotrauma.Networking QueueID = id; CanSend = canSend; CanReceive = canReceive; - LatestBufferID = BUFFER_COUNT-1; + LatestBufferID = BUFFER_COUNT - 1; firstRead = true; LastReadTime = DateTime.Now; @@ -84,7 +90,7 @@ namespace Barotrauma.Networking public void EnqueueBuffer(int length) { - if (length > byte.MaxValue) return; + if (length > byte.MaxValue) { return; } newestBufferInd = (newestBufferInd + 1) % BUFFER_COUNT; @@ -92,17 +98,18 @@ namespace Barotrauma.Networking bufferLengths[newestBufferInd] = length; BufferToQueue.CopyTo(buffers[newestBufferInd], 0); - - if ((enqueuedTotalLength+length)>0) LatestBufferID++; + + if ((enqueuedTotalLength + length) > 0) { LatestBufferID++; } } - public void RetrieveBuffer(int id,out int outSize,out byte[] outBuf) + public void RetrieveBuffer(int id, out int outSize, out byte[] outBuf) { lock (buffers) { if (id >= LatestBufferID - (BUFFER_COUNT - 1) && id <= LatestBufferID) { - int index = (newestBufferInd - (LatestBufferID - id)); if (index < 0) index += BUFFER_COUNT; + int index = newestBufferInd - (LatestBufferID - id); + if (index < 0) { index += BUFFER_COUNT; } outSize = bufferLengths[index]; outBuf = buffers[index]; return; @@ -114,9 +121,10 @@ namespace Barotrauma.Networking public virtual void Write(IWriteMessage msg) { - if (!CanSend) throw new Exception("Called Write on a VoipQueue not set up for sending"); + if (!CanSend) { throw new Exception("Called Write on a VoipQueue not set up for sending"); } msg.Write((UInt16)LatestBufferID); + msg.Write(ForceLocal); msg.WritePadBits(); for (int i = 0; i < BUFFER_COUNT; i++) { int index = (newestBufferInd + i + 1) % BUFFER_COUNT; @@ -126,13 +134,15 @@ namespace Barotrauma.Networking } } - public virtual bool Read(IReadMessage msg) + public virtual bool Read(IReadMessage msg, bool discardData = false) { - if (!CanReceive) throw new Exception("Called Read on a VoipQueue not set up for receiving"); + if (!CanReceive) { throw new Exception("Called Read on a VoipQueue not set up for receiving"); } UInt16 incLatestBufferID = msg.ReadUInt16(); - if (firstRead || NetIdUtils.IdMoreRecent(incLatestBufferID,LatestBufferID)) + if ((firstRead || NetIdUtils.IdMoreRecent(incLatestBufferID, LatestBufferID)) && !discardData) { + ForceLocal = msg.ReadBoolean(); msg.ReadPadBits(); + firstRead = false; for (int i = 0; i < BUFFER_COUNT; i++) { @@ -146,6 +156,7 @@ namespace Barotrauma.Networking } else { + msg.ReadBoolean(); msg.ReadPadBits(); for (int i = 0; i < BUFFER_COUNT; i++) { byte len = msg.ReadByte(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index 974d8e9a1..2a0115df7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -88,6 +88,8 @@ namespace Barotrauma Circle, Rectangle, Capsule, HorizontalCapsule }; + public const float DefaultAngularDamping = 5.0f; + private static readonly List list = new List(); public static List List { @@ -342,7 +344,7 @@ namespace Barotrauma FarseerBody.BodyType = BodyType.Dynamic; FarseerBody.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel; FarseerBody.CollisionCategories = Physics.CollisionCharacter; - FarseerBody.AngularDamping = 5.0f; + FarseerBody.AngularDamping = DefaultAngularDamping; FarseerBody.FixedRotation = true; FarseerBody.Friction = 0.05f; FarseerBody.Restitution = 0.05f; @@ -370,15 +372,15 @@ namespace Barotrauma list.Add(this); } - public PhysicsBody(XElement element, Vector2 position, float scale=1.0f) + public PhysicsBody(XElement element, Vector2 position, float scale = 1.0f) { float radius = ConvertUnits.ToSimUnits(element.GetAttributeFloat("radius", 0.0f)) * scale; float height = ConvertUnits.ToSimUnits(element.GetAttributeFloat("height", 0.0f)) * scale; float width = ConvertUnits.ToSimUnits(element.GetAttributeFloat("width", 0.0f)) * scale; density = element.GetAttributeFloat("density", 10.0f); CreateBody(width, height, radius, density); - //Enum.TryParse(element.GetAttributeString("bodytype", "Dynamic"), out BodyType bodyType); - FarseerBody.BodyType = BodyType.Dynamic; + Enum.TryParse(element.GetAttributeString("bodytype", "Dynamic"), out BodyType bodyType); + FarseerBody.BodyType = bodyType; FarseerBody.CollisionCategories = Physics.CollisionItem; FarseerBody.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionPlatform; FarseerBody.Friction = element.GetAttributeFloat("friction", 0.3f); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs index be0e35b25..1be30c7bb 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/GameScreen.cs @@ -36,7 +36,6 @@ namespace Barotrauma public GameScreen() { - Camera cam; #if CLIENT cam = new Camera(); cam.Translate(new Vector2(-10.0f, 50.0f)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs index 2526e7f8c..37b24658a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/SerializableProperty.cs @@ -679,7 +679,7 @@ namespace Barotrauma { foreach (XElement subElement in configElement.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() != "upgrade") { continue; } + if (!subElement.Name.ToString().Equals("upgrade", StringComparison.OrdinalIgnoreCase)) { continue; } var upgradeVersion = new Version(subElement.GetAttributeString("gameversion", "0.0.0.0")); if (savedVersion >= upgradeVersion) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index 8daa9feef..3c3f636d2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -570,7 +570,7 @@ namespace Barotrauma { bool hexFailed = true; stringColor = stringColor.Trim(); - if (stringColor[0]=='#') + if (stringColor.Length > 0 && stringColor[0] == '#') { stringColor = stringColor.Substring(1); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs index cc3e4649d..b1aaba6f6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Sprite/Sprite.cs @@ -47,7 +47,11 @@ namespace Barotrauma //the offset used when drawing the sprite protected Vector2 offset; - private bool lazyLoad; + public bool LazyLoad + { + get; + private set; + } protected Vector2 origin; @@ -135,7 +139,7 @@ namespace Barotrauma public Sprite(XElement element, string path = "", string file = "", bool lazyLoad = false) { if (element == null) { return; } - this.lazyLoad = lazyLoad; + this.LazyLoad = lazyLoad; SourceElement = element; if (!ParseTexturePath(path, file)) { return; } Name = SourceElement.GetAttributeString("name", null); @@ -329,10 +333,10 @@ namespace Barotrauma { foreach (XElement subElement in SourceElement.Elements()) { - if (subElement.Name.ToString().ToLowerInvariant() == "override") + if (subElement.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { string language = subElement.GetAttributeString("language", ""); - if (TextManager.Language.ToLower() == language.ToLower()) + if (TextManager.Language.Equals(language, StringComparison.InvariantCultureIgnoreCase)) { return subElement; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs index aeeea37c0..7eb494f1f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs @@ -21,7 +21,8 @@ namespace Barotrauma HasTag, HasStatusTag, Affliction, - EntityType + EntityType, + LimbType } public enum Comparison @@ -51,7 +52,8 @@ namespace Barotrauma // Only used by attacks public readonly bool TargetSelf; - private readonly string[] afflictionNames = new string[] { "internaldamage", "bleeding", "burn", "oxygenlow", "bloodloss", "pressure", "stun", "husk", "afflictionhusk", "huskinfection" }; + // Only used by conditionals targeting an item (makes the conditional check the item/character whose inventory this item is inside) + public readonly bool TargetContainer; private readonly int cancelStatusEffect; @@ -62,6 +64,7 @@ namespace Barotrauma { case "targetitemcomponent": case "targetself": + case "targetcontainer": return false; default: return true; @@ -132,6 +135,7 @@ namespace Barotrauma } TargetItemComponentName = attribute.Parent.GetAttributeString("targetitemcomponent", ""); + TargetContainer = attribute.Parent.GetAttributeBool("targetcontainer", false); TargetSelf = attribute.Parent.GetAttributeBool("targetself", false); foreach (XElement subElement in attribute.Parent.Elements()) @@ -150,13 +154,9 @@ namespace Barotrauma if (!Enum.TryParse(AttributeName, true, out Type)) { - if (afflictionNames.Any(n => n == AttributeName)) + if (AfflictionPrefab.Prefabs.Any(p => p.Identifier.Equals(AttributeName, StringComparison.OrdinalIgnoreCase))) { Type = ConditionType.Affliction; - if (AttributeName == "husk" || AttributeName == "huskaffliction") - { - AttributeName = "huskinfection"; - } } else { @@ -173,8 +173,7 @@ namespace Barotrauma public bool Matches(ISerializableEntity target) { - string valStr = AttributeValue.ToString(); - + string valStr = AttributeValue.ToString(); switch (Type) { case ConditionType.PropertyValue: @@ -264,34 +263,47 @@ namespace Barotrauma default: return false; } - case ConditionType.Affliction: - if (target == null) { return Operator == OperatorType.NotEquals; } - - Character targetChar = target as Character; - if (target is Limb limb) { targetChar = limb.character; } - if (targetChar != null) + case ConditionType.LimbType: { - var health = targetChar.CharacterHealth; - if (health == null) { return false; } - var affliction = health.GetAffliction(AttributeName); - float afflictionStrength = affliction == null ? 0.0f : affliction.Strength; - if (FloatValue.HasValue) + if (!(target is Limb limb)) { - float value = FloatValue.Value; - switch (Operator) + return false; + } + else + { + return limb.type.ToString().Equals(valStr, StringComparison.OrdinalIgnoreCase); + } + } + case ConditionType.Affliction: + { + if (target == null) { return Operator == OperatorType.NotEquals; } + + Character targetChar = target as Character; + if (target is Limb limb) { targetChar = limb.character; } + if (targetChar != null) + { + var health = targetChar.CharacterHealth; + if (health == null) { return false; } + var affliction = health.GetAffliction(AttributeName); + float afflictionStrength = affliction == null ? 0.0f : affliction.Strength; + if (FloatValue.HasValue) { - case OperatorType.Equals: - return afflictionStrength == value; - case OperatorType.GreaterThan: - return afflictionStrength > value; - case OperatorType.GreaterThanEquals: - return afflictionStrength >= value; - case OperatorType.LessThan: - return afflictionStrength < value; - case OperatorType.LessThanEquals: - return afflictionStrength <= value; - case OperatorType.NotEquals: - return afflictionStrength != value; + float value = FloatValue.Value; + switch (Operator) + { + case OperatorType.Equals: + return afflictionStrength == value; + case OperatorType.GreaterThan: + return afflictionStrength > value; + case OperatorType.GreaterThanEquals: + return afflictionStrength >= value; + case OperatorType.LessThan: + return afflictionStrength < value; + case OperatorType.LessThanEquals: + return afflictionStrength <= value; + case OperatorType.NotEquals: + return afflictionStrength != value; + } } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 4f877f611..bc28e243b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -14,7 +14,7 @@ namespace Barotrauma public readonly Entity Entity; public readonly List Targets; public Character User { get; private set; } - + public float Timer; public DurationListElement(StatusEffect parentEffect, Entity parentEntity, IEnumerable targets, float duration, Character user) @@ -24,15 +24,15 @@ namespace Barotrauma Targets = new List(targets); Timer = duration; User = user; - } - + } + public void Reset(float duration, Character newUser) { Timer = duration; User = newUser; } } - + partial class StatusEffect { [Flags] @@ -71,10 +71,10 @@ namespace Barotrauma //backwards compatibility DebugConsole.ThrowError("Error in StatusEffect config (" + element.ToString() + ") - use item identifier instead of the name."); string itemPrefabName = element.GetAttributeString("name", ""); - ItemPrefab = ItemPrefab.Prefabs.Find(m => m.NameMatches(itemPrefabName) || m.Tags.Contains(itemPrefabName)); + ItemPrefab = ItemPrefab.Prefabs.Find(m => m.NameMatches(itemPrefabName, StringComparison.InvariantCultureIgnoreCase) || m.Tags.Contains(itemPrefabName)); if (ItemPrefab == null) { - DebugConsole.ThrowError("Error in StatusEffect \""+ parentDebugName + "\" - item prefab \"" + itemPrefabName + "\" not found."); + DebugConsole.ThrowError("Error in StatusEffect \"" + parentDebugName + "\" - item prefab \"" + itemPrefabName + "\" not found."); } } else @@ -92,7 +92,7 @@ namespace Barotrauma return; } } - + Speed = element.GetAttributeFloat("speed", 0.0f); Rotation = MathHelper.ToRadians(element.GetAttributeFloat("rotation", 0.0f)); @@ -111,11 +111,16 @@ namespace Barotrauma [Serialize("", false)] public string SpeciesName { get; private set; } + [Serialize(1, false)] public int Count { get; private set; } + [Serialize(0f, false)] public float Spread { get; private set; } + [Serialize("0,0", false)] + public Vector2 Offset { get; private set; } + public CharacterSpawnInfo(XElement element, string parentDebugName) { SerializableProperties = SerializableProperty.DeserializeProperties(this, element); @@ -130,7 +135,7 @@ namespace Barotrauma protected HashSet targetIdentifiers; private readonly List requiredItems; - + public readonly string[] propertyNames; private readonly object[] propertyEffects; @@ -138,11 +143,11 @@ namespace Barotrauma private readonly List propertyConditionals; private readonly bool setValue; - + private readonly bool disableDeltaTime; - + private readonly HashSet tags; - + private readonly float duration; private readonly float lifeTime; private float lifeTimer; @@ -154,7 +159,7 @@ namespace Barotrauma public readonly bool Stackable = true; //Can the same status effect be applied several times to the same targets? private readonly int useItemCount; - + private readonly bool removeItem, removeCharacter; public readonly ActionType type = ActionType.OnActive; @@ -168,15 +173,15 @@ namespace Barotrauma public readonly float FireSize; - public readonly LimbType targetLimb; - + public readonly LimbType[] targetLimbs; + public readonly float SeverLimbsProbability; public HashSet TargetIdentifiers { get { return targetIdentifiers; } } - + public List Afflictions { get; @@ -192,6 +197,8 @@ namespace Barotrauma private set; } + public Vector2 Offset { get; private set; } + public string Tags { get { return string.Join(",", tags); } @@ -206,7 +213,6 @@ namespace Barotrauma string newTag = tag.Trim(); if (!tags.Contains(newTag)) tags.Add(newTag); } - } } @@ -230,10 +236,16 @@ namespace Barotrauma tags = new HashSet(element.GetAttributeString("tags", "").Split(',')); Range = element.GetAttributeFloat("range", 0.0f); - string targetLimbName = element.GetAttributeString("targetlimb", null); - if (targetLimbName != null) + Offset = element.GetAttributeVector2("offset", Vector2.Zero); + string[] targetLimbNames = element.GetAttributeStringArray("targetlimb", null) ?? element.GetAttributeStringArray("targetlimbs", null); + if (targetLimbNames != null) { - Enum.TryParse(targetLimbName, out targetLimb); + List targetLimbs = new List(); + foreach (string targetLimbName in targetLimbNames) + { + if (Enum.TryParse(targetLimbName, out LimbType targetLimb)) { targetLimbs.Add(targetLimb); } + } + if (targetLimbs.Count > 0) { this.targetLimbs = targetLimbs.ToArray(); } } IEnumerable attributes = element.Attributes(); @@ -394,7 +406,7 @@ namespace Barotrauma float afflictionStrength = subElement.GetAttributeFloat(1.0f, "amount", "strength"); Afflictions.Add(afflictionPrefab.Instantiate(afflictionStrength)); - + break; case "reduceaffliction": if (subElement.Attribute("name") != null) @@ -493,41 +505,68 @@ namespace Barotrauma } } - public virtual bool HasRequiredConditions(IEnumerable targets) + public bool HasRequiredConditions(IEnumerable targets) + { + return HasRequiredConditions(targets, targetingContainer: false); + } + + private bool HasRequiredConditions(IEnumerable targets, bool targetingContainer) { if (!propertyConditionals.Any()) { return true; } - if (requiredItems.Any() && requiredItems.All(ri => ri.MatchOnEmpty) && targets.Count() == 0) { return true; } + if (requiredItems.Any() && requiredItems.All(ri => ri.MatchOnEmpty) && !targets.Any()) { return true; } switch (conditionalComparison) { case PropertyConditional.Comparison.Or: - foreach (ISerializableEntity target in targets) + foreach (PropertyConditional pc in propertyConditionals) { - foreach (PropertyConditional pc in propertyConditionals) + if (pc.TargetContainer && !targetingContainer) { - if (!string.IsNullOrEmpty(pc.TargetItemComponentName)) + var target = targets.FirstOrDefault(t => t is Item || t is ItemComponent); + var targetItem = target as Item ?? (target as ItemComponent)?.Item; + if (targetItem?.ParentInventory == null) { continue; } + if (targetItem.ParentInventory.Owner is Item container && HasRequiredConditions(container.AllPropertyObjects, targetingContainer: true)) { return true; } + if (targetItem.ParentInventory.Owner is Character character && HasRequiredConditions(character.ToEnumerable(), targetingContainer: true)) { return true; } + } + else + { + foreach (ISerializableEntity target in targets) { - if (!(target is ItemComponent ic) || ic.Name != pc.TargetItemComponentName) + if (!string.IsNullOrEmpty(pc.TargetItemComponentName)) { - continue; + if (!(target is ItemComponent ic) || ic.Name != pc.TargetItemComponentName) + { + continue; + } } + if (pc.Matches(target)) { return true; } } - if (pc.Matches(target)) { return true; } } } return false; case PropertyConditional.Comparison.And: - foreach (ISerializableEntity target in targets) + foreach (PropertyConditional pc in propertyConditionals) { - foreach (PropertyConditional pc in propertyConditionals) + if (pc.TargetContainer && !targetingContainer) { - if (!string.IsNullOrEmpty(pc.TargetItemComponentName)) + var target = targets.FirstOrDefault(t => t is Item || t is ItemComponent); + var targetItem = target as Item ?? (target as ItemComponent)?.Item; + if (targetItem?.ParentInventory == null) { return false; } + if (targetItem.ParentInventory.Owner is Item container && !HasRequiredConditions(container.AllPropertyObjects, targetingContainer: true)) { return false; } + if (targetItem.ParentInventory.Owner is Character character && !HasRequiredConditions(character.ToEnumerable(), targetingContainer: true)) { return false; } + } + else + { + foreach (ISerializableEntity target in targets) { - if (!(target is ItemComponent ic) || ic.Name != pc.TargetItemComponentName) + if (!string.IsNullOrEmpty(pc.TargetItemComponentName)) { - continue; + if (!(target is ItemComponent ic) || ic.Name != pc.TargetItemComponentName) + { + continue; + } } + if (!pc.Matches(target)) { return false; } } - if (!pc.Matches(target)) { return false; } } } return true; @@ -580,13 +619,16 @@ namespace Barotrauma if (this.type != type || !HasRequiredItems(entity)) { return; } if (targetIdentifiers != null && !IsValidTarget(target)) { return; } - + if (duration > 0.0f && !Stackable) { //ignore if not stackable and there's already an identical statuseffect DurationListElement existingEffect = DurationList.Find(d => d.Parent == this && d.Targets.FirstOrDefault() == target); - existingEffect?.Reset(Math.Max(existingEffect.Timer, duration), user); - return; + if (existingEffect != null) + { + existingEffect.Reset(Math.Max(existingEffect.Timer, duration), user); + return; + } } if (!HasRequiredConditions(target.ToEnumerable())) { return; } @@ -645,18 +687,19 @@ namespace Barotrauma hull = ((Item)entity).CurrentHull; } - Vector2 position = worldPosition ?? entity.WorldPosition; - if (targetLimb != LimbType.None) + Vector2 position = worldPosition ?? (entity.Removed ? Vector2.Zero : entity.WorldPosition); + if (targetLimbs?.FirstOrDefault(l => l != LimbType.None) is LimbType l) { if (entity is Character c) { - Limb limb = c.AnimController.GetLimb(targetLimb); + Limb limb = c.AnimController.GetLimb(l); if (limb != null && !limb.Removed) { position = limb.WorldPosition; } } } + position += Offset; foreach (ISerializableEntity serializableEntity in targets) { @@ -675,13 +718,13 @@ namespace Barotrauma if (item.Removed) continue; item.Use(deltaTime, targetCharacter, targets.FirstOrDefault(t => t is Limb) as Limb); } - } + } if (removeItem) { foreach (var target in targets) { - if (target is Item item) { Entity.Spawner?.AddToRemoveQueue(item); } + if (target is Item item) { Entity.Spawner?.AddToRemoveQueue(item); } } } if (removeCharacter) @@ -707,11 +750,11 @@ namespace Barotrauma for (int i = 0; i < propertyNames.Length; i++) { - if (target == null || target.SerializableProperties == null || + if (target == null || target.SerializableProperties == null || !target.SerializableProperties.TryGetValue(propertyNames[i], out SerializableProperty property)) continue; ApplyToProperty(target, property, propertyEffects[i], deltaTime); } - } + } } if (explosion != null && entity != null) @@ -732,6 +775,7 @@ namespace Barotrauma character.LastDamageSource = entity; foreach (Limb limb in character.AnimController.Limbs) { + if (targetLimbs != null && !targetLimbs.Contains(limb.type)) { continue; } limb.character.DamageLimb(position, limb, multipliedAffliction.ToEnumerable(), stun: 0.0f, playSound: false, attackImpulse: 0.0f, attacker: affliction.Source); limb.character.TrySeverLimbJoints(limb, SeverLimbsProbability); //only apply non-limb-specific afflictions to the first limb @@ -764,8 +808,15 @@ namespace Barotrauma { float prevVitality = targetCharacter.Vitality; targetCharacter.CharacterHealth.ReduceAffliction(targetLimb, reduceAffliction.First, reduceAmount); + if (user != null && user != targetCharacter) + { + if (!targetCharacter.IsDead) + { + targetCharacter.TryAdjustAttackerSkill(user, targetCharacter.Vitality - prevVitality); + } + }; #if SERVER - GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, user, prevVitality - targetCharacter.Vitality); + GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, user, prevVitality - targetCharacter.Vitality, 0.0f); #endif } } @@ -776,7 +827,7 @@ namespace Barotrauma var fire = new FireSource(position, hull); fire.Size = new Vector2(FireSize, fire.Size.Y); } - + bool isNotClient = GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient; if (isNotClient && entity != null && Entity.Spawner != null) //clients are not allowed to spawn entities { @@ -785,15 +836,15 @@ namespace Barotrauma var characters = new List(); for (int i = 0; i < characterSpawnInfo.Count; i++) { - Entity.Spawner.AddToSpawnQueue(characterSpawnInfo.SpeciesName, position + Rand.Vector(characterSpawnInfo.Spread, Rand.RandSync.Server), + Entity.Spawner.AddToSpawnQueue(characterSpawnInfo.SpeciesName, position + Rand.Vector(characterSpawnInfo.Spread, Rand.RandSync.Server) + characterSpawnInfo.Offset, onSpawn: newCharacter => - { - characters.Add(newCharacter); - if (characters.Count == characterSpawnInfo.Count) { - SwarmBehavior.CreateSwarm(characters.Cast()); - } - }); + characters.Add(newCharacter); + if (characters.Count == characterSpawnInfo.Count) + { + SwarmBehavior.CreateSwarm(characters.Cast()); + } + }); } } foreach (ItemSpawnInfo itemSpawnInfo in spawnItems) @@ -804,7 +855,7 @@ namespace Barotrauma Entity.Spawner.AddToSpawnQueue(itemSpawnInfo.ItemPrefab, position); break; case ItemSpawnInfo.SpawnPositionType.ThisInventory: - { + { if (entity is Character character) { if (character.Inventory != null && character.Inventory.Items.Any(it => it == null)) @@ -843,7 +894,7 @@ namespace Barotrauma Entity.Spawner.AddToSpawnQueue(itemSpawnInfo.ItemPrefab, containedInventory); break; } - } + } } break; } @@ -857,21 +908,24 @@ namespace Barotrauma private void ApplyToProperty(ISerializableEntity target, SerializableProperty property, object value, float deltaTime) { - if (disableDeltaTime || setValue) deltaTime = 1.0f; - + if (disableDeltaTime || setValue) { deltaTime = 1.0f; } Type type = value.GetType(); - if (type == typeof(float) || - (type == typeof(int) && property.GetValue(target) is float)) + if (type == typeof(float) || (type == typeof(int) && property.GetValue(target) is float)) { float floatValue = Convert.ToSingle(value) * deltaTime; - - if (!setValue) floatValue += (float)property.GetValue(target); + if (!setValue) + { + floatValue += (float)property.GetValue(target); + } property.TrySetValue(target, floatValue); } else if (type == typeof(int) && value is int) { int intValue = (int)((int)value * deltaTime); - if (!setValue) intValue += (int)property.GetValue(target); + if (!setValue) + { + intValue += (int)property.GetValue(target); + } property.TrySetValue(target, intValue); } else if (type == typeof(bool) && value is bool) @@ -904,8 +958,8 @@ namespace Barotrauma continue; } - element.Targets.RemoveAll(t => - (t is Entity entity && entity.Removed) || + element.Targets.RemoveAll(t => + (t is Entity entity && entity.Removed) || (t is Limb limb && (limb.character == null || limb.character.Removed))); if (element.Targets.Count == 0) { @@ -960,8 +1014,15 @@ namespace Barotrauma { float prevVitality = targetCharacter.Vitality; targetCharacter.CharacterHealth.ReduceAffliction(targetLimb, reduceAffliction.First, reduceAffliction.Second * deltaTime); + if (element.User != null && element.User != targetCharacter) + { + if (!targetCharacter.IsDead) + { + targetCharacter.TryAdjustAttackerSkill(element.User, targetCharacter.Vitality - prevVitality); + } + }; #if SERVER - GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, element.User, prevVitality - targetCharacter.Vitality); + GameMain.Server.KarmaManager.OnCharacterHealthChanged(targetCharacter, element.User, prevVitality - targetCharacter.Vitality, 0.0f); #endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs index 3f58a0392..f7d234cc0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/SteamAchievementManager.cs @@ -122,7 +122,10 @@ namespace Barotrauma if (GameMain.GameSession != null) { #if CLIENT - if (Character.Controlled != null) { CheckMidRoundAchievements(Character.Controlled); } + if (Character.Controlled != null && !(GameMain.GameSession.GameMode is SubTestMode)) + { + CheckMidRoundAchievements(Character.Controlled); + } #else foreach (Client client in GameMain.Server.ConnectedClients) { @@ -222,7 +225,7 @@ namespace Barotrauma 1); } - roundData.Casualties.Add(character); + roundData?.Casualties.Add(character); UnlockAchievement(causeOfDeath.Killer, "kill" + character.SpeciesName); if (character.CurrentHull != null) @@ -288,7 +291,7 @@ namespace Barotrauma public static void OnRoundEnded(GameSession gameSession) { //made it to the destination - if (gameSession.Submarine.AtEndPosition && Level.Loaded != null) + if (gameSession?.Submarine != null && Level.Loaded != null && gameSession.Submarine.AtEndPosition) { float levelLengthMeters = Physics.DisplayToRealWorldRatio * Level.Loaded.Size.X; float levelLengthKilometers = levelLengthMeters / 1000.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs b/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs index 7b96f06e4..d27d9d074 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/TextManager.cs @@ -222,6 +222,13 @@ namespace Barotrauma } } +#if DEBUG + if (GameMain.Config != null && GameMain.Config.TextManagerDebugModeEnabled) + { + return textTag; + } +#endif + foreach (TextPack textPack in textPacks[Language]) { string text = textPack.Get(textTag); @@ -297,6 +304,7 @@ namespace Barotrauma { for (int i = 0; i < variableTags.Length; i++) { + if (string.IsNullOrEmpty(variableValues[i])) { continue; } if (formatCapitals[i]) { variableValues[i] = HandleVariableCapitalization(text, variableTags[i], variableValues[i]); @@ -306,6 +314,13 @@ namespace Barotrauma for (int i = 0; i < variableTags.Length; i++) { + if (variableValues[i] == null) + { +#if DEBUG + DebugConsole.ThrowError("Error in TextManager.GetWithVariables (variable " + i + " was null).\n" + Environment.StackTrace); +#endif + continue; + } text = text.Replace(variableTags[i], variableValues[i]); } @@ -636,6 +651,37 @@ namespace Barotrauma } } + /// + /// Fetches a single variable from a servermessage + /// + public static string GetServerMessageVariable(string message, string variable) + { + int variableIndex = message.IndexOf(variable); + if (variableIndex == -1) + { +#if DEBUG + DebugConsole.ThrowError($"Server message variable: '{variable}' not found in message: '{message}'"); +#endif + return string.Empty; + } + + int startIndex = message.IndexOf('=', variableIndex) + 1; + int endIndex = startIndex; + + for (int i = startIndex; i < message.Length; i++) + { + if (message[i] == '/' || message[i] == '~') + { + endIndex = i; + break; + } + } + + if (endIndex == startIndex) endIndex = message.Length; + + return message.Substring(startIndex, endIndex - startIndex); + } + public static bool IsServerMessageWithVariables(string message) { for (int i = 0; i < serverMessageCharacters.Length; i++) diff --git a/Barotrauma/BarotraumaShared/SharedSource/TextPack.cs b/Barotrauma/BarotraumaShared/SharedSource/TextPack.cs index ff7efc692..26ad16718 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/TextPack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/TextPack.cs @@ -30,7 +30,7 @@ namespace Barotrauma { doc = XMLExtensions.TryLoadXml(filePath); if (doc != null) { break; } - if (filePath.ToLowerInvariant() == "content/texts/englishvanilla.xml") + if (filePath.Equals("content/texts/englishvanilla.xml", StringComparison.OrdinalIgnoreCase)) { //try fixing legacy EnglishVanilla path string newPath = "Content/Texts/English/EnglishVanilla.xml"; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/RichTextData.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/RichTextData.cs new file mode 100644 index 000000000..eb4fac829 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/RichTextData.cs @@ -0,0 +1,85 @@ +using Microsoft.Xna.Framework; +using System.Collections.Generic; + +namespace Barotrauma +{ + public class RichTextData + { + public int StartIndex, EndIndex; + public Color? Color; + public string Metadata; + + private const char definitionIndicator = '‖'; + private const char attributeSeparator = ';'; + private const char keyValueSeparator = ':'; + //private const char lineChangeIndicator = '\n'; + + private const string colorDefinition = "color"; + private const string metadataDefinition = "metadata"; + private const string endDefinition = "end"; + + public static List GetRichTextData(string text, out string sanitizedText) + { + List textColors = null; + sanitizedText = text; + if (!string.IsNullOrEmpty(text) && text.Contains(definitionIndicator)) + { + string[] segments = text.Split(definitionIndicator); + + sanitizedText = string.Empty; + + textColors = new List(); + RichTextData tempData = null; + + int prevIndex = 0; + int currIndex = 0; + for (int i=0;i onCompletion) { - AddInternal(task, (Task t, object obj) => { onCompletion(t); }, null); + AddInternal(task, (Task t, object obj) => { onCompletion?.Invoke(t); }, null); } public static void Add(Task task, U userdata, Action onCompletion) where U : class { - AddInternal(task, (Task t, object obj) => { onCompletion(t, (U)obj); }, userdata); + AddInternal(task, (Task t, object obj) => { onCompletion?.Invoke(t, (U)obj); }, userdata); } public static void Add(Task task, Action> onCompletion) { - AddInternal(task, (Task t, object obj) => { onCompletion((Task)t); }, null); + AddInternal(task, (Task t, object obj) => { onCompletion?.Invoke((Task)t); }, null); } public static void Add(Task task, U userdata, Action, U> onCompletion) where U : class { - AddInternal(task, (Task t, object obj) => { onCompletion((Task)t, (U)obj); }, userdata); + AddInternal(task, (Task t, object obj) => { onCompletion?.Invoke((Task)t, (U)obj); }, userdata); } public static void Update() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs index 69aaede2d..e8ff1a8e9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs @@ -1,13 +1,14 @@ -using System; +using Barotrauma.Extensions; +using Barotrauma.Networking; +using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; -using System.Security.Cryptography; using System.Reflection; +using System.Security.Cryptography; using System.Text; -using Microsoft.Xna.Framework; -using Barotrauma.Networking; -using System.Diagnostics; namespace Barotrauma { @@ -151,7 +152,7 @@ namespace Barotrauma public static string RemoveInvalidFileNameChars(string fileName) { - var invalidChars = Path.GetInvalidFileNameChars(); + var invalidChars = Path.GetInvalidFileNameChars().Concat(new char[] {':', ';'}); foreach (char invalidChar in invalidChars) { fileName = fileName.Replace(invalidChar.ToString(), ""); @@ -589,5 +590,12 @@ namespace Barotrauma _ => t, }; } + + public static Rectangle GetWorldBounds(Point center, Point size) + { + Point halfSize = size.Divide(2); + Point topLeft = new Point(center.X - halfSize.X, center.Y + halfSize.Y); + return new Rectangle(topLeft, size); + } } } diff --git a/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub b/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub new file mode 100644 index 000000000..c36cc0868 Binary files /dev/null and b/Barotrauma/BarotraumaShared/Submarines/Azimuth.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Berilia.sub b/Barotrauma/BarotraumaShared/Submarines/Berilia.sub index 4968e0976..0bc3d9353 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Berilia.sub and b/Barotrauma/BarotraumaShared/Submarines/Berilia.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Dugong.sub b/Barotrauma/BarotraumaShared/Submarines/Dugong.sub index bb3483879..2e9861259 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Dugong.sub and b/Barotrauma/BarotraumaShared/Submarines/Dugong.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub b/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub index 7aa3d0166..6589ee31a 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub and b/Barotrauma/BarotraumaShared/Submarines/Hemulen.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Humpback.sub b/Barotrauma/BarotraumaShared/Submarines/Humpback.sub index 950e13ff9..4fa4b3639 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Humpback.sub and b/Barotrauma/BarotraumaShared/Submarines/Humpback.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub b/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub index 0fefa6c94..81e94e034 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub and b/Barotrauma/BarotraumaShared/Submarines/Kastrull.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub b/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub index 7524f6b98..7ffd2212b 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub and b/Barotrauma/BarotraumaShared/Submarines/KastrullDrone.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Orca.sub b/Barotrauma/BarotraumaShared/Submarines/Orca.sub index d51353ce9..a3ca955c9 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Orca.sub and b/Barotrauma/BarotraumaShared/Submarines/Orca.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Remora.sub b/Barotrauma/BarotraumaShared/Submarines/Remora.sub index 9a91693e5..d3e55e1f8 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Remora.sub and b/Barotrauma/BarotraumaShared/Submarines/Remora.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Selkie.sub b/Barotrauma/BarotraumaShared/Submarines/Selkie.sub index fa017357e..fa5eeb553 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Selkie.sub and b/Barotrauma/BarotraumaShared/Submarines/Selkie.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Typhon.sub b/Barotrauma/BarotraumaShared/Submarines/Typhon.sub index 5ef283918..b382f330a 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Typhon.sub and b/Barotrauma/BarotraumaShared/Submarines/Typhon.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub b/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub index 2925bd7cb..f6f1ec06f 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub and b/Barotrauma/BarotraumaShared/Submarines/Typhon2.sub differ diff --git a/Barotrauma/BarotraumaShared/Submarines/Venture.sub b/Barotrauma/BarotraumaShared/Submarines/Venture.sub index 20a6a4c51..d76eaff15 100644 Binary files a/Barotrauma/BarotraumaShared/Submarines/Venture.sub and b/Barotrauma/BarotraumaShared/Submarines/Venture.sub differ diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index d812c454b..2b485ee9c 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,148 @@ +--------------------------------------------------------------------------------------------------------- +v0.9.9.0 +--------------------------------------------------------------------------------------------------------- + +- Added wrecked submarines to levels. +- Reimplemented carrier (now called Thalamus). +- New submarine, Azimuth. +- Miscellaneous performance optimizations. +- Improvements to traitor missions (slightly simpler, with clearer instructions). +- Reduced the skill requirements for mechanical repairs. +- Reduced the damage when a mechanical repair fails. +- Rebalanced mission rewards. +- New water ambience sounds. +- Speed up despawning when there are lots of bodies inside the sub, enemies despawn x2 faster. +- Salvage missions can be completed by taking the artifact to the start outpost (the descriptions don't specify which outpost to return it to). +- Characters with insufficient skills can fail at mechanical repairs, causing minor injuries. +- Gaps that are inside a hull don't flood the sub, a warning icon is displayed on those gaps in the sub editor. +- Added submarine test mode to the sub editor. +- Moved the entity filter panel and "previously used" panel in the sub editor to the top left corner of the screen. +- Pressing enter after modifying the value of a text field in the sub editor is no longer required. +- "teleportsub" console command can be used to teleport the sub to the position of the cursor. +- The engine vibrates and plays a loud sound when it's damaged to indicate more clearly that it needs repairs. +- Added animated lights to alarm buzzers and sirens. +- Sonar beacon's label can be edited in-game. +- Assistants gain skills faster than other characters. +- Skills increase faster during non-campaign rounds. +- More noticeable particle effects on damaged devices. +- Added "itemdamage" parameter to the explosion command. +- Huskified characters turn to the final stage faster. +- Using the "dumptofile" and "findentityids" commands doesn't require a permission from the server. +- Added Scale and Color properties to DecorativeSprites. +- Added OnDamaged status effect type. +- Added lights that indicate the state of a docking port. +- Significantly increased the item damages of all explosives. +- Replaced the old husk stinger with tentacles (similar to Husked Crawler). +- Minor adjustments to Husk, Human Husk, and Husked Crawler. +- Modding: Added "probabilitymultiplier" attribute for damagemodifiers. Can be used to make items/armor affect the probability of getting an affliction. +- Diving suits now give some protection against husk infections. +- Light components can be set to flicker in the sub editor. +- Fixed odd movements when pressing the ragdoll button while stunned. +- Made it easier to interact with doors that are overlapping with a docking port/hatch. +- Option to disable bot conversations in multiplayer in the server config file. + +Submarine editor improvements: +- Removed character mode. All the functionality of the character mode is now supported in the default mode. +- It's possible to modify the properties of multiple selected entities at the same time. +- Autosaving (the submarine is automatically saved to a temporary file which can be recovered if the game crashes). +- Added hotkeys for a bunch of actions (the hotkeys are visible in the tooltips). +- Pressing F focuses the camera on the selected entities. +- Control + A to select/deselect everything. +- Hold shift to ignore the grid when placing / resizing. +- The outlines of all wires are shown in wiring mode. +- Orphaned wires are deleted automatically. +- The content of the search box isn't reset when switching entity categories. +- Mouse middle mouse dragging is now 1:1, previously the view moved too fast. +- Linked submarines now have visuals when dragging. +- Holding down arrow keys now continues to move the entity after a small delay. +- Changing the background color by shift right clicking. + +Antigriefing improvements: +- Players can be kicked/banned/muted/votekicked and their ranks changed by right clicking the name in the crew list or chat. +- Karma penalty for stunning: gets progressively more severe the more stuns a player causes. +- Stealing weapons or ID cards from stunned/unconscious characters reduces karma. +- Added karma category to server log. + +Tab menu improvements: +- Improved layout. +- Show the roles of the players (moderator, admin, host) in the player list. +- Characters can be muted/kicked/banned from the player list. +- List the players who have joined/left the server. +- Display spectators in the list. +- Show player pings in the list. + +Command interface improvements: +- Contextual commands: characters can be ordered to operate/repair/use specific items by holding shift while enabling the command interface. +- Job icons are shown in the command interface to make it a little faster to differentiate between characters. +- Added separate orders for repairing electrical and mechanical devices. + +VOIP improvements: +- Added a keybind for local voice chat (= it's possible to only speak to players next to you without everyone within headset range hearing it). +- Added an adjustable delay for cutting audio capture after the push-to-talk key has been released. +- Fixed audio being suppressed when someone speaks even if VOIP volume is low or completely muted. + +Workshop improvements: +- "Enabling" a mod through the ingame Workshop screen is no longer a thing; subscribing to a mod is all you need to do for the game to install it once it's downloaded. +- Loading preview images and installing mods is done asynchronously (= no lag spikes). +- Added notifications to the main menu to indicate when mods are being downloaded and have been installed. +- Files are automatically added to the Workshop item publish menu as they're added to a to-be-published content package's directory. + +Bugfixes: +- Fixed achievements not unlocking. +- Fixed positions of artifacts spawning in caves and on level walls getting desynced between the server and clients. +- Fixed new wire node being created at an item client-side if connecting the wire fails due to an electric shock server-side. +- Fixed clients executing console commands they don't have permission to use. +- Fixed enablecheats command not being relayed to server. +- Fixed light component and alarm siren/buzzer states occasionally getting desynced. +- Fixed inability to enter the sub through very small hulls. +- Fixed antibiotics not giving husk infection resistance when shot from a syringe gun. +- Fixed text overflows in the player management panel in the server lobby in languages other than English. +- Fixed searchlight toggle doing nothing. +- Fixed hulls that have minuscule amounts of water in them (too small to be even rendered) being able to trigger InWater effects and water footstep sounds. +- Fixed pumps dicarding the previously received set_targetlevel signal after 0.1 seconds, preventing manual control systems from working if the pumps aren't receiving a continuous set_targetlevel signal. +- Fixes to occasional crashes when rendering alien artifacts. +- Fixed radio chat key not working. +- Fixed reconnected clients not gaining XP. +- Fixes to sprite depth issues in Berilia's decorative fin structures. +- Fixed bots with an active order not switching to idle state when they have nothing to do, causing them to stand in place or walk against a wall. +- Fixed docking ports without a door sometimes getting linked to an incorrect door, preventing the door's linked gap from working correctly. +- Fixed monsters not spawning in ruins if the submarine hasn't left the starting outpost. +- Fixed freezing caused by SoundManager.InitStreamThread. +- Fixed generic Powered components (= charging dock) always using the default power consumption value defined in XML even if the power consumption is changed in the sub editor. +- Fixed last traitor objective's end message not being shown. +- Fixed command interface showing non-interactable devices as valid targets. +- Fixed dragged characters sometimes getting stuck on staircases. +- Fixed serious performance issues triggered by bot's combat and rescue objectives when there are no safe hulls left. +- Fixed characters being grabbable through walls. +- Potentially fixed a crash in GameMain.WindowActive caused by voice chat capture when the game is exiting. +- Spawn cargo above the floor structure of the cargo room, not above the bottom of the cargo hull. Fixes items spawning partially inside structures where the hull extends below the floor. +- Fixed all monsters bleeding red blood. +- Fixed light sprite's scale not being taken into account in light culling, causing lights with a large scaled-up light sprite to disappear before they're off-screen. +- Fixed damageable items not taking damage from repair tools. +- Fixed hitscan projectiles not hitting items. +- Fixed the short freeze when switching to the sub editor. +- Fixed sprites not being included in the xml element when using "copy to clipboard" in the particle editor. +- Fixed clients being able to use the number inputs in the multiplayer campaign store without the appropriate permissions. +- Fixed ability to buy more than 100 items of a kind despite 100 being the limit of how many purchased items of a kind can spawn. +- Fixed handheld sonar pinging when LMB is held, quickly draining the battery. +- Added some checks to prevent character sounds from crashing the game when audio playback is disabled. +- Fixed character collider's angular damping getting set to 0 if the character gets frozen, which caused the character's swimming animation to get wobbly. +- Fixed private messages not having the [PM] tag when dead. +- Fixed inability to open the health interface when hovering the cursor over another character, even if the character's health interface is inaccessible. +- Fixed bots being inactive when far away from all player characters. +- Fixed some particles (like muzzle flash) not being drawn on top of structures that are outside hulls. +- Fixed enemies being unable to target entities outside the submarine if they are inside it. +- Fixed oxygenite shards and tanks causing characters to move at turbo speed. +- Fixed waypoint links to gaps, doors, and ladders etc being removed when linking waypoint to another in the sub editor. Allow to remove links between waypoints. +- Fixed "teleportsub" console command teleporting also the connected outposts. +- Fixed characters getting stuck on platforms that extend outside the sub. +- Fixed diving suit lockers that have been recolored in the sub editor switching back to the default color when a suit is placed inside them. +- Coilgun/railgun loaders don't deteriorate if launching the turret fails. Fixes loaders deteriorating rapidly if the turret is receiving a continuous signal. +- Fixed rendering glitches on the surface of water when there's steep angles on the surface. +- Fixed inability to fire ranged reapons from sub to another through docking ports. +- Fixed inability drag characters from sub to another through docking ports. +- Fixed bots trying to shoot targets inside the alien ruins. + --------------------------------------------------------------------------------------------------------- v0.9.8.0 --------------------------------------------------------------------------------------------------------- @@ -50,7 +195,6 @@ Bugfixes: - Fixed crashing if the selected core content package contains errors (missing files, invalid XML files). - Fixed splash screen causing a crash on some Mac systems. - Fixed corrupted save files causing the game to crash during loading. -- Fixed a memory leak in the health interface that may have caused crashes on very long game sessions. - Fixed clients being able to run the "enablecheats" command client-side without the permission to use the command. - Fixed AI inputs not being synced with clients, preventing clients from seeing when the bots aim/shoot/attack. - Don't allow using the "flipx" console command while playing online. diff --git a/Barotrauma/BarotraumaShared/serversettings.xml b/Barotrauma/BarotraumaShared/serversettings.xml index 4960cd465..4830b170b 100644 --- a/Barotrauma/BarotraumaShared/serversettings.xml +++ b/Barotrauma/BarotraumaShared/serversettings.xml @@ -43,10 +43,10 @@ endvoterequiredratio="0.6" kickvoterequiredratio="0.6" killdisconnectedtime="120" - kickafktime="120" + kickafktime="600" traitoruseratio="True" traitorratio="0.2" - karmaenabled="False" + karmaenabled="True" karmapreset="default" gamemodeidentifier="sandbox" missiontype="Random" diff --git a/Libraries/Facepunch.Steamworks/Enum/DebugOutputType.cs b/Libraries/Facepunch.Steamworks/Enum/DebugOutputType.cs index 5f36bbdee..d94bb6361 100644 --- a/Libraries/Facepunch.Steamworks/Enum/DebugOutputType.cs +++ b/Libraries/Facepunch.Steamworks/Enum/DebugOutputType.cs @@ -1,6 +1,6 @@ namespace Steamworks.Data { - enum DebugOutputType : int + public enum DebugOutputType : int { None = 0, Bug = 1, // You used the API incorrectly, or an internal error happened diff --git a/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Win64.csproj b/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Win64.csproj index 6f3e0e832..6ab41d502 100644 --- a/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Win64.csproj +++ b/Libraries/Facepunch.Steamworks/Facepunch.Steamworks.Win64.csproj @@ -31,6 +31,22 @@ git + + 1701;1702;1591;1587 + + + + 1701;1702;1591;1587 + + + + 1701;1702;1591;1587 + + + + 1701;1702;1591;1587 + + diff --git a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingUtils.cs b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingUtils.cs index 8da70497d..8f168e163 100644 --- a/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingUtils.cs +++ b/Libraries/Facepunch.Steamworks/Generated/Interfaces/ISteamNetworkingUtils.cs @@ -204,11 +204,11 @@ namespace Steamworks #region FunctionMeta [UnmanagedFunctionPointer( Platform.MemberConvention )] - private delegate void FSetDebugOutputFunction( IntPtr self, DebugOutputType eDetailLevel, FSteamNetworkingSocketsDebugOutput pfnFunc ); + private delegate void FSetDebugOutputFunction( IntPtr self, DebugOutputType eDetailLevel, IntPtr pfnFunc ); private FSetDebugOutputFunction _SetDebugOutputFunction; #endregion - internal void SetDebugOutputFunction( DebugOutputType eDetailLevel, FSteamNetworkingSocketsDebugOutput pfnFunc ) + internal void SetDebugOutputFunction( DebugOutputType eDetailLevel, IntPtr pfnFunc ) { _SetDebugOutputFunction( Self, eDetailLevel, pfnFunc ); } diff --git a/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs b/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs index 6ea3cdc45..1c7a09c8e 100644 --- a/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs +++ b/Libraries/Facepunch.Steamworks/SteamNetworkingUtils.cs @@ -81,6 +81,11 @@ namespace Steamworks public static long LocalTimestamp => Internal.GetLocalTimestamp(); + public static void SetDebugOutputFunction(DebugOutputType eDetailLevel, IntPtr pfnFunc) + { + Internal.SetDebugOutputFunction(eDetailLevel, pfnFunc); + } + /// /// [0 - 100] - Randomly discard N pct of packets diff --git a/Libraries/Facepunch.Steamworks/SteamUgc.cs b/Libraries/Facepunch.Steamworks/SteamUgc.cs index 5420d3796..4d4bf5c75 100644 --- a/Libraries/Facepunch.Steamworks/SteamUgc.cs +++ b/Libraries/Facepunch.Steamworks/SteamUgc.cs @@ -39,6 +39,7 @@ namespace Steamworks ItemInstalled_t.Install(x => { if (x.AppID == SteamClient.AppId) { + GlobalOnItemInstalled?.Invoke(x.PublishedFileId); if (onItemInstalled?.ContainsKey(x.PublishedFileId) ?? false) { onItemInstalled[x.PublishedFileId]?.Invoke(); @@ -92,5 +93,9 @@ namespace Steamworks } private static Dictionary onItemInstalled; + + public static event Action GlobalOnItemInstalled; + + public static uint NumSubscribedItems { get { return Internal.GetNumSubscribedItems(); } } } } \ No newline at end of file diff --git a/Libraries/Facepunch.Steamworks/Structs/SteamNetworking.cs b/Libraries/Facepunch.Steamworks/Structs/SteamNetworking.cs index 3b8f78673..22868b48c 100644 --- a/Libraries/Facepunch.Steamworks/Structs/SteamNetworking.cs +++ b/Libraries/Facepunch.Steamworks/Structs/SteamNetworking.cs @@ -5,7 +5,8 @@ using System.Text; namespace Steamworks.Data { - delegate void FSteamNetworkingSocketsDebugOutput (DebugOutputType nType, string pszMsg ); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void FSteamNetworkingSocketsDebugOutput (DebugOutputType nType, string pszMsg ); public struct SteamNetworkingPOPID { diff --git a/Libraries/Facepunch.Steamworks/Utility/Helpers.cs b/Libraries/Facepunch.Steamworks/Utility/Helpers.cs index 3d1ea74c0..5c585e4df 100644 --- a/Libraries/Facepunch.Steamworks/Utility/Helpers.cs +++ b/Libraries/Facepunch.Steamworks/Utility/Helpers.cs @@ -9,31 +9,36 @@ namespace Steamworks { public const int MaxStringSize = 1024 * 32; + private static object mutex = new object(); private static IntPtr[] MemoryPool; private static int MemoryPoolIndex; public static unsafe IntPtr TakeMemory() { - if ( MemoryPool == null ) + IntPtr take = IntPtr.Zero; + lock (mutex) { - // - // The pool has 5 items. This should be safe because we shouldn't really - // ever be using more than 2 memory pools - // - MemoryPool = new IntPtr[5]; + if (MemoryPool == null) + { + // + // The pool has 5 items. This should be safe because we shouldn't really + // ever be using more than 2 memory pools + // + MemoryPool = new IntPtr[5]; - for ( int i = 0; i < MemoryPool.Length; i++ ) - MemoryPool[i] = Marshal.AllocHGlobal( MaxStringSize ); + for (int i = 0; i < MemoryPool.Length; i++) + MemoryPool[i] = Marshal.AllocHGlobal(MaxStringSize); + } + + MemoryPoolIndex++; + if (MemoryPoolIndex >= MemoryPool.Length) + MemoryPoolIndex = 0; + + take = MemoryPool[MemoryPoolIndex]; + + ((byte*)take)[0] = 0; } - MemoryPoolIndex++; - if ( MemoryPoolIndex >= MemoryPool.Length ) - MemoryPoolIndex = 0; - - var take = MemoryPool[MemoryPoolIndex]; - - ((byte*)take)[0] = 0; - return take; } diff --git a/Libraries/GameAnalytics/GA_SDK_NETSTANDARD/GA_SDK_NETSTANDARD.csproj b/Libraries/GameAnalytics/GA_SDK_NETSTANDARD/GA_SDK_NETSTANDARD.csproj index 5fcec6050..cd6803b0e 100644 --- a/Libraries/GameAnalytics/GA_SDK_NETSTANDARD/GA_SDK_NETSTANDARD.csproj +++ b/Libraries/GameAnalytics/GA_SDK_NETSTANDARD/GA_SDK_NETSTANDARD.csproj @@ -26,7 +26,7 @@ - + diff --git a/Libraries/Lidgren.Network/Lidgren.NetStandard.csproj b/Libraries/Lidgren.Network/Lidgren.NetStandard.csproj index 66cb930fb..3e265503b 100644 --- a/Libraries/Lidgren.Network/Lidgren.NetStandard.csproj +++ b/Libraries/Lidgren.Network/Lidgren.NetStandard.csproj @@ -10,6 +10,22 @@ AnyCPU;x64 + + 1701;1702;3021 + + + + 1701;1702;3021 + + + + 1701;1702;3021 + + + + 1701;1702;3021 + + diff --git a/Libraries/Lidgren.Network/NetBuffer.cs b/Libraries/Lidgren.Network/NetBuffer.cs index 34c783fde..15903bb73 100644 --- a/Libraries/Lidgren.Network/NetBuffer.cs +++ b/Libraries/Lidgren.Network/NetBuffer.cs @@ -76,7 +76,7 @@ namespace Lidgren.Network MethodInfo[] methods = typeof(NetIncomingMessage).GetMethods(BindingFlags.Instance | BindingFlags.Public); foreach (MethodInfo mi in methods) { - if (mi.GetParameters().Length == 0 && mi.Name.StartsWith("Read", StringComparison.InvariantCulture) && mi.Name.Substring(4) == mi.ReturnType.Name) + if (mi.GetParameters().Length == 0 && mi.Name.StartsWith("Read", StringComparison.OrdinalIgnoreCase) && mi.Name.Substring(4) == mi.ReturnType.Name) { s_readMethods[mi.ReturnType] = mi; } @@ -86,7 +86,7 @@ namespace Lidgren.Network methods = typeof(NetOutgoingMessage).GetMethods(BindingFlags.Instance | BindingFlags.Public); foreach (MethodInfo mi in methods) { - if (mi.Name.Equals("Write", StringComparison.InvariantCulture)) + if (mi.Name.Equals("Write", StringComparison.OrdinalIgnoreCase)) { ParameterInfo[] pis = mi.GetParameters(); if (pis.Length == 1) diff --git a/Libraries/Lidgren.Network/NetPeer.Internal.cs b/Libraries/Lidgren.Network/NetPeer.Internal.cs index 8375dca6e..d2765ade2 100644 --- a/Libraries/Lidgren.Network/NetPeer.Internal.cs +++ b/Libraries/Lidgren.Network/NetPeer.Internal.cs @@ -132,7 +132,7 @@ namespace Lidgren.Network m_socket.ReceiveBufferSize = m_configuration.ReceiveBufferSize; m_socket.SendBufferSize = m_configuration.SendBufferSize; m_socket.Blocking = false; - m_socket.DualMode = true; + m_socket.DualMode = m_configuration.UseDualModeSockets; var ep = (EndPoint)new NetEndPoint(m_configuration.LocalAddress.MapToIPv6(), reBind ? m_listenPort : m_configuration.Port); m_socket.Bind(ep); diff --git a/Libraries/Lidgren.Network/NetPeerConfiguration.cs b/Libraries/Lidgren.Network/NetPeerConfiguration.cs index 56836fef7..b714edae2 100644 --- a/Libraries/Lidgren.Network/NetPeerConfiguration.cs +++ b/Libraries/Lidgren.Network/NetPeerConfiguration.cs @@ -142,6 +142,12 @@ namespace Lidgren.Network get { return m_appIdentifier; } } + public bool UseDualModeSockets + { + get; + set; + } = true; + /// /// Enables receiving of the specified type of message /// diff --git a/Libraries/Lidgren.Network/NetUtility.cs b/Libraries/Lidgren.Network/NetUtility.cs index 349d1150b..0c437ce7f 100644 --- a/Libraries/Lidgren.Network/NetUtility.cs +++ b/Libraries/Lidgren.Network/NetUtility.cs @@ -401,7 +401,7 @@ namespace Lidgren.Network { if (j >= h) { - if (string.Compare(list[j - h].Name, tmp.Name, StringComparison.InvariantCulture) > 0) + if (string.Compare(list[j - h].Name, tmp.Name, StringComparison.OrdinalIgnoreCase) > 0) { list[j] = list[j - h]; j -= h; diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs index 983a84dd7..f32bad148 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatch.cs @@ -238,6 +238,44 @@ namespace Microsoft.Xna.Framework.Graphics } } + public void Draw(Texture2D texture, VertexPositionColorTexture[] vertices, float layerDepth) + { + CheckValid(texture); + + float sortKey = 0f; + + // set SortKey based on SpriteSortMode. + switch (_sortMode) + { + // Comparison of Texture objects. + case SpriteSortMode.Texture: + sortKey = texture.SortingKey; + break; + // Comparison of Depth + case SpriteSortMode.FrontToBack: + sortKey = layerDepth; + break; + // Comparison of Depth in reverse + case SpriteSortMode.BackToFront: + sortKey = -layerDepth; + break; + } + + int iters = vertices.Length / 4; + for (int i=0;i /// Submit a sprite for drawing in the current batch. /// @@ -1220,6 +1258,7 @@ namespace Microsoft.Xna.Framework.Graphics } } } + _batcher.Dispose(); base.Dispose(disposing); } } diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatchItem.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatchItem.cs index 68e23646c..d0ebb45e5 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatchItem.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatchItem.cs @@ -22,7 +22,7 @@ namespace Microsoft.Xna.Framework.Graphics vertexBL = new VertexPositionColorTexture(); vertexBR = new VertexPositionColorTexture(); } - + public void Set ( float x, float y, float dx, float dy, float w, float h, float sin, float cos, Color color, Vector2 texCoordTL, Vector2 texCoordBR, float depth ) { // TODO, Should we be just assigning the Depth Value to Z? diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatcher.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatcher.cs index 7dd5cc934..25814aece 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatcher.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/SpriteBatcher.cs @@ -13,7 +13,7 @@ namespace Microsoft.Xna.Framework.Graphics /// batched and will process them into short.MaxValue groups (strided by 6 for the number of vertices /// sent to the GPU). /// - internal class SpriteBatcher + internal class SpriteBatcher : IDisposable { /* * Note that this class is fundamental to high performance for SpriteBatch games. Please exercise @@ -54,6 +54,9 @@ namespace Microsoft.Xna.Framework.Graphics private VertexPositionColorTexture[] _vertexArray; + private VertexBuffer vertexBuffer; + private IndexBuffer indexBuffer; + public SpriteBatcher (GraphicsDevice device) { _device = device; @@ -136,6 +139,12 @@ namespace Microsoft.Xna.Framework.Graphics _index = newIndex; _vertexArray = new VertexPositionColorTexture[4 * numBatchItems]; + + indexBuffer?.Dispose(); + vertexBuffer?.Dispose(); + indexBuffer = new IndexBuffer(_device, IndexElementSize.SixteenBits, _index.Length, BufferUsage.WriteOnly); + indexBuffer.SetData(_index); + vertexBuffer = new VertexBuffer(_device, VertexPositionColorTexture.VertexDeclaration, _vertexArray.Length, BufferUsage.WriteOnly); } /// @@ -225,7 +234,8 @@ namespace Microsoft.Xna.Framework.Graphics } // return items to the pool. _batchItemCount = 0; - } + _device.Textures[0] = null; + } /// /// Sends the triangle list to the graphics device. Here is where the actual drawing starts. @@ -241,6 +251,7 @@ namespace Microsoft.Xna.Framework.Graphics var vertexCount = end - start; + _device.Indices = indexBuffer; // If the effect is not null, then apply each pass and render the geometry if (effect != null) { @@ -252,31 +263,26 @@ namespace Microsoft.Xna.Framework.Graphics // Whatever happens in pass.Apply, make sure the texture being drawn // ends up in Textures[0]. _device.Textures[0] = texture; - - _device.DrawUserIndexedPrimitives( - PrimitiveType.TriangleList, - _vertexArray, - 0, - vertexCount, - _index, - 0, - (vertexCount / 4) * 2, - VertexPositionColorTexture.VertexDeclaration); + vertexBuffer.SetData(_vertexArray, start, vertexCount); + _device.SetVertexBuffer(vertexBuffer); + _device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, (vertexCount / 4) * 2); } } else { // If no custom effect is defined, then simply render. - _device.DrawUserIndexedPrimitives( - PrimitiveType.TriangleList, - _vertexArray, - 0, - vertexCount, - _index, - 0, - (vertexCount / 4) * 2, - VertexPositionColorTexture.VertexDeclaration); + vertexBuffer.SetData(_vertexArray, start, vertexCount); + _device.SetVertexBuffer(vertexBuffer); + _device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, (vertexCount / 4) * 2); } + + _device.Indices = null; + } + + public void Dispose() + { + indexBuffer?.Dispose(); + vertexBuffer?.Dispose(); } } } diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/Texture2D.DirectX.cs b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/Texture2D.DirectX.cs index 2cce0c726..93f4c9363 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/Texture2D.DirectX.cs +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Graphics/Texture2D.DirectX.cs @@ -74,7 +74,10 @@ namespace Microsoft.Xna.Framework.Graphics lock (d3dContext) { d3dContext.UpdateSubresource(GetTexture(), subresourceIndex, region, dataPtr, GetPitch(w), 0); - d3dContext.GenerateMips(GetShaderResourceView()); + if (_mipmap) + { + d3dContext.GenerateMips(GetShaderResourceView()); + } } } finally diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Windows.NetStandard.csproj b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Windows.NetStandard.csproj index 0b4d267a9..a98cc5944 100644 --- a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Windows.NetStandard.csproj +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/MonoGame.Framework.Windows.NetStandard.csproj @@ -500,19 +500,18 @@ + + + + - - - - - diff --git a/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Properties/launchSettings.json b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Properties/launchSettings.json new file mode 100644 index 000000000..42118d872 --- /dev/null +++ b/Libraries/MonoGame.Framework/Src/MonoGame.Framework/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "MonoGame.Framework.Windows.NetStandard": { + "commandName": "Project", + "nativeDebugging": false + } + } +} \ No newline at end of file diff --git a/Libraries/SharpFont/Source/SharpFont/SharpFont.NetStandard.csproj b/Libraries/SharpFont/Source/SharpFont/SharpFont.NetStandard.csproj index f1c3685fa..6f2368424 100644 --- a/Libraries/SharpFont/Source/SharpFont/SharpFont.NetStandard.csproj +++ b/Libraries/SharpFont/Source/SharpFont/SharpFont.NetStandard.csproj @@ -20,6 +20,7 @@ TRACE;DEBUG;SHARPFONT_PORTABLE true + 1701;1702;3021 @@ -30,6 +31,7 @@ TRACE;SHARPFONT_PORTABLE true + 1701;1702;3021